fix: prevent duplicate diagnostic task creation

Serialize cross-parent diagnostic claims at project scope and reuse a recent active canonical task. Keep ordinary, completed, and path-distinct work independent.
This commit is contained in:
gsxdsm
2026-07-18 21:21:55 -07:00
parent e30f6d851f
commit 74de68af72
7 changed files with 222 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Prevent agents from filing duplicate active diagnostic follow-ups discovered by different tasks.
category: fix
dev: Adds narrow code-identifier-based cross-parent deduplication while preserving distinct and completed work.

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { computeParentIntentClaimId, findSameAgentDuplicates, flagSameAgentDuplicate } from "../duplicate-intake.js";
import { computeCrossParentDiagnosticClaimId, computeParentIntentClaimId, findSameAgentDuplicates, flagSameAgentDuplicate } from "../duplicate-intake.js";
import type { TaskStore } from "../store.js";
describe("findSameAgentDuplicates", () => {
@@ -122,6 +122,44 @@ describe("findSameAgentDuplicates", () => {
expect(claim("Add new support")).toMatch(/^agent-parent-intent:FN-8277:[a-f0-9]{64}$/);
});
it("derives one cross-parent claim for the recently repeated diagnostic", () => {
const descriptions = [
"Investigate and repair dashboard typecheck failure: app/utils/capture-screenshot.ts imports unresolved `html2canvas`, causing `pnpm verify:fast` to fail.",
"Fix dashboard typecheck failure: app/utils/capture-screenshot.ts cannot resolve the html2canvas module during pnpm verify:fast.",
"Fix dashboard typecheck failure: packages/dashboard/app/utils/capture-screenshot.ts imports unresolved html2canvas. Add or correctly wire the dependency.",
"Fix dashboard typecheck missing html2canvas dependency/import in packages/dashboard/app/utils/capture-screenshot.ts (TS2307 observed during pnpm verify:fast).",
"Fix dashboard typecheck dependency resolution for app/utils/capture-screenshot.ts: Cannot find module 'html2canvas' during pnpm verify:fast.",
"Restore the missing `html2canvas` dependency declaration/lock entry for dashboard screenshot capture so @fusion/dashboard typecheck passes.",
];
const claims = descriptions.map((description) => computeCrossParentDiagnosticClaimId({ description }));
expect(new Set(claims).size).toBe(1);
expect(claims[0]).toMatch(/^agent-diagnostic-intent:/);
});
it("does not globally claim ordinary work or unrelated work on the same module", () => {
expect(computeCrossParentDiagnosticClaimId({
description: "Add screenshot upload support using html2canvas",
})).toBeNull();
expect(computeCrossParentDiagnosticClaimId({
description: "Improve html2canvas capture performance",
})).toBeNull();
});
it("keeps diagnostic paths distinct when they share a basename", () => {
const first = computeCrossParentDiagnosticClaimId({
description: "Fix unresolved packages/alpha/index.ts typecheck failure.",
});
const second = computeCrossParentDiagnosticClaimId({
description: "Fix unresolved packages/beta/index.ts typecheck failure.",
});
expect(first).not.toBeNull();
expect(second).not.toBeNull();
expect(first).not.toBe(second);
});
it("does not match sibling with different parent task", () => {
const matches = findSameAgentDuplicates(
{

View File

@@ -78,6 +78,49 @@ export function computeParentIntentClaimId(input: SameAgentDuplicateInput): stri
return parentId && anchor ? `agent-parent-intent:${parentId}:${anchor}` : null;
}
const DIAGNOSTIC_ACTION_PATTERN = /\b(?:fix|investigate|repair|resolve|restore)\b/i;
const DIAGNOSTIC_FAILURE_PATTERN = /\b(?:cannot|can't|error|fail(?:ed|ure|s)?|missing|ts\d{4}|typecheck|unresolved)\b/i;
const DIAGNOSTIC_OBJECT_PATTERNS = [
/\b(?:missing|unresolved)\s+([`'"]?[@a-z0-9][@a-z0-9._/-]*[`'"]?)/gi,
/\b(?:cannot|can't)\s+(?:find|resolve)(?:\s+module)?\s+(?:the\s+)?([`'"]?[@a-z0-9][@a-z0-9._/-]*[`'"]?)/gi,
];
const IGNORED_DIAGNOSTIC_OBJECTS = new Set(["a", "an", "dependency", "module", "the", "type", "types"]);
function normalizeDiagnosticObject(value: string): string | null {
const normalized = value.toLowerCase().replace(/^[`'"]+|[`'".,;:]+$/g, "");
if (!normalized || IGNORED_DIAGNOSTIC_OBJECTS.has(normalized)) {
return null;
}
// Only code-shaped objects are safe for global convergence. Generic prose
// such as "missing button" must remain scoped to its parent task.
return /[0-9@./_-]/.test(normalized) ? normalized : null;
}
/**
* Stable claim for an active diagnostic follow-up that may be discovered by
* several unrelated parent tasks. This is intentionally narrower than general
* near-duplicate matching: only repair-shaped failures with a code identifier
* can converge globally.
*/
export function computeCrossParentDiagnosticClaim(input: Pick<SameAgentDuplicateInput, "title" | "description">): { id: string; searchTerm: string } | null {
const text = `${input.title ?? ""}\n${input.description}`;
if (!DIAGNOSTIC_ACTION_PATTERN.test(text) || !DIAGNOSTIC_FAILURE_PATTERN.test(text)) return null;
const objects = DIAGNOSTIC_OBJECT_PATTERNS.flatMap((pattern) =>
[...text.matchAll(pattern)]
.map((match) => normalizeDiagnosticObject(match[1] ?? ""))
.filter((value): value is string => value !== null),
);
const diagnosticObject = [...new Set(objects)].sort()[0];
if (!diagnosticObject) return null;
const fingerprint = computeContentFingerprint({ title: "agent-diagnostic-intent", description: diagnosticObject });
return fingerprint ? { id: `agent-diagnostic-intent:${fingerprint}`, searchTerm: diagnosticObject } : null;
}
export function computeCrossParentDiagnosticClaimId(input: Pick<SameAgentDuplicateInput, "title" | "description">): string | null {
return computeCrossParentDiagnosticClaim(input)?.id ?? null;
}
/**
* Find candidate tasks that look like duplicates spawned by the same caller.
*

View File

@@ -725,6 +725,8 @@ export type { TaskDependencyMutation } from "./store.js";
export {
findSameAgentDuplicates,
computeParentIntentClaimId,
computeCrossParentDiagnosticClaim,
computeCrossParentDiagnosticClaimId,
archiveAsSameAgentDuplicate,
flagSameAgentDuplicate,
flagTriageDuplicate,

View File

@@ -768,6 +768,8 @@ export type { TaskDependencyMutation } from "./store.js";
export {
findSameAgentDuplicates,
computeParentIntentClaimId,
computeCrossParentDiagnosticClaim,
computeCrossParentDiagnosticClaimId,
archiveAsSameAgentDuplicate,
flagSameAgentDuplicate,
flagTriageDuplicate,

View File

@@ -14,6 +14,8 @@ function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
getRootDir: vi.fn().mockReturnValue("/project"),
searchTasks: vi.fn().mockResolvedValue([]),
findRecentTasksBySourceParentTaskId: vi.fn().mockResolvedValue([]),
findRecentTasksByContentFingerprint: vi.fn().mockResolvedValue([]),
updateTask: vi.fn(),
@@ -368,6 +370,92 @@ describe("createDelegateTaskTool", () => {
expect(taskStore.moveTask).not.toHaveBeenCalled();
});
it("reuses one active diagnostic follow-up across different parent tasks", async () => {
const tasks: Task[] = [];
vi.mocked(taskStore.searchTasks).mockImplementation(async () => tasks);
vi.mocked(taskStore.findRecentTasksBySourceParentTaskId).mockImplementation(async (parentId) =>
tasks.filter((task) => task.sourceParentTaskId === parentId),
);
vi.mocked(taskStore.createTask).mockImplementation(async (input) => {
await Promise.resolve();
const now = new Date().toISOString();
const created = {
id: `FN-${tasks.length + 1}`,
description: input.description,
dependencies: [],
column: "triage" as const,
sourceParentTaskId: input.source?.sourceParentTaskId,
steps: [],
currentStep: 0,
log: [],
createdAt: now,
updatedAt: now,
} as Task;
tasks.push(created);
return created;
});
const [first, replay] = await Promise.all([
createAgentTask(taskStore, {
description: "Investigate and repair dashboard typecheck failure: app/utils/capture-screenshot.ts imports unresolved `html2canvas`, causing `pnpm verify:fast` to fail.",
}, { sourceTaskId: "FN-8343", rootDir: "/worktrees/FN-8343" }),
createAgentTask(taskStore, {
description: "Restore the missing `html2canvas` dependency declaration/lock entry for dashboard screenshot capture so @fusion/dashboard typecheck passes.",
}, { sourceTaskId: "FN-8348", rootDir: "/worktrees/FN-8348" }),
]);
expect(first.wasDuplicate).toBe(false);
expect(replay).toMatchObject({ wasDuplicate: true, task: { id: first.task.id } });
expect(tasks).toHaveLength(1);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: expect.objectContaining({
sourceMetadata: expect.objectContaining({
crossParentDiagnosticClaimId: expect.stringMatching(/^agent-diagnostic-intent:/),
}),
}),
}), expect.anything());
});
it("does not let a completed diagnostic suppress newly required work", async () => {
const completed = {
id: "FN-DONE",
description: "Fix unresolved `html2canvas` typecheck failure.",
dependencies: [],
column: "done" as const,
sourceParentTaskId: "FN-OLD-PARENT",
steps: [],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as Task;
const created = {
...completed,
id: "FN-NEW",
column: "triage" as const,
sourceParentTaskId: "FN-NEW-PARENT",
} as Task;
vi.mocked(taskStore.searchTasks).mockResolvedValue([completed]);
vi.mocked(taskStore.createTask).mockResolvedValue(created);
const result = await createAgentTask(taskStore, {
description: "Restore the missing html2canvas dependency so dashboard typecheck passes.",
}, { sourceTaskId: "FN-NEW-PARENT" });
expect(result).toEqual({ task: created, wasDuplicate: false });
expect(taskStore.createTask).toHaveBeenCalledOnce();
});
it("fails closed when cross-parent diagnostic lookup is unavailable", async () => {
vi.mocked(taskStore.searchTasks).mockRejectedValue(new Error("database unavailable"));
await expect(createAgentTask(taskStore, {
description: "Fix unresolved `html2canvas` typecheck failure.",
}, { sourceTaskId: "FN-PARENT" })).rejects.toThrow("Unable to verify cross-parent diagnostic task uniqueness");
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("persists option-based parent provenance on the step-session fn_task_create surface", async () => {
const tool = createTaskCreateTool(taskStore, undefined, { sourceTaskId: "FN-PARENT", sourceAgentId: "agent-worker" });
await tool.execute("call-1", { description: "Capture optional report screenshots" }, undefined as any, undefined as any, undefined as any);

View File

@@ -16,7 +16,7 @@ import * as fusionCore from "@fusion/core";
import type { AgentState, AgentCapability, AgentUpdateInput, AgentLogEntry, Artifact, ArtifactCreateInput, ArtifactWithTask, Task, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings, ChatStore, WorkflowSettingDefinition, GoalStatus, WorkflowIrNode } from "@fusion/core";
import { listTraits, isBuiltinWorkflowId, AgentStore, validateColumnAgentBindings, ColumnAgentBindingError, stripApprovalBypassFlags, WorkflowSettingRejectionError, resolveEffectiveSettingsById, resolveWorkflowIrById, findOrphanedSettingValues, BUILTIN_WORKFLOW_SETTINGS, MAX_TASK_LIST_TEXT_CHARS, formatCurrentTaskLine, normalizeWorkflowIcon, parseWorkflowIr, WorkflowIrError, assertColumnTraitsValid, ColumnTraitValidationError } from "@fusion/core";
import { promoteHeldTask } from "./hold-release.js";
import { computeParentIntentClaimId, DASHBOARD_USER_ID, dailyMemoryPath, ensureOpenClawMemoryFiles, evaluateImplementationTaskBind, extractAgentProvisioningRequest, findSameAgentDuplicates, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, reconcileDeterministicDuplicate, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTaskGithubTracking, runDeterministicDuplicateGuard, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
import { computeCrossParentDiagnosticClaim, computeCrossParentDiagnosticClaimId, computeParentIntentClaimId, DASHBOARD_USER_ID, dailyMemoryPath, ensureOpenClawMemoryFiles, evaluateImplementationTaskBind, extractAgentProvisioningRequest, findSameAgentDuplicates, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, reconcileDeterministicDuplicate, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTaskGithubTracking, runDeterministicDuplicateGuard, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
import { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
import { ResearchStepRunner } from "./research-step-runner.js";
@@ -975,6 +975,13 @@ export async function createAgentTask(
const rootDir = options?.rootDir;
const sourceParentTaskId = (input.source?.sourceParentTaskId ?? options?.sourceTaskId)?.trim().toUpperCase();
const sourceAgentId = input.source?.sourceAgentId ?? options?.sourceAgentId;
const crossParentDiagnosticClaim = options?.bypassDuplicateCheck === true
? null
: computeCrossParentDiagnosticClaim({ title: input.title, description: input.description });
const crossParentDiagnosticClaimId = crossParentDiagnosticClaim?.id ?? null;
const duplicateLockScope = crossParentDiagnosticClaimId
? store.getRootDir?.() ?? rootDir ?? "agent-tools"
: rootDir ?? store.getRootDir?.() ?? "agent-tools";
const effectiveSource = input.source || sourceParentTaskId || sourceAgentId
? {
sourceType: input.source?.sourceType ?? "api" as const,
@@ -987,11 +994,11 @@ export async function createAgentTask(
title: input.title,
description: input.description,
}, {
lockScope: rootDir ?? store.getRootDir?.() ?? "agent-tools",
lockScope: duplicateLockScope,
bypass: options?.bypassDuplicateCheck === true,
acknowledgedDuplicates: options?.acknowledgedDuplicates,
serializationKey: sourceParentTaskId ? `parent:${sourceParentTaskId}` : undefined,
sourceParentTaskId,
serializationKey: crossParentDiagnosticClaimId ?? (sourceParentTaskId ? `parent:${sourceParentTaskId}` : undefined),
sourceParentTaskId: crossParentDiagnosticClaimId ? null : sourceParentTaskId,
logger: log,
});
@@ -1003,6 +1010,35 @@ export async function createAgentTask(
};
}
if (crossParentDiagnosticClaim) {
try {
const acknowledged = new Set(options?.acknowledgedDuplicates ?? []);
const cutoffMs = Date.now() - 24 * 60 * 60 * 1000;
const candidates = (await store.searchTasks(crossParentDiagnosticClaim.searchTerm, {
slim: true,
includeArchived: false,
}))
.filter((candidate) => candidate.column !== "done" && candidate.column !== "archived")
.filter((candidate) => Date.parse(candidate.createdAt) >= cutoffMs)
.filter((candidate) => !acknowledged.has(candidate.id))
.filter((candidate) => computeCrossParentDiagnosticClaimId({
title: candidate.title,
description: candidate.description,
}) === crossParentDiagnosticClaimId)
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
const canonical = candidates[0];
if (canonical) {
return { task: await carryCanonicalTaskRouting(store, canonical, input), wasDuplicate: true };
}
} catch (error) {
log.warn("Cross-parent diagnostic duplicate pre-check failed; aborting creation", {
crossParentDiagnosticClaimId,
error: error instanceof Error ? error.message : String(error),
});
throw new Error("Unable to verify cross-parent diagnostic task uniqueness", { cause: error });
}
}
if (sourceParentTaskId && options?.bypassDuplicateCheck !== true) {
try {
const acknowledged = new Set(options?.acknowledgedDuplicates ?? []);
@@ -1037,6 +1073,7 @@ export async function createAgentTask(
const sourceMetadata = {
...(effectiveSource?.sourceMetadata ?? {}),
...(guard.fingerprint ? { contentFingerprint: guard.fingerprint } : {}),
...(crossParentDiagnosticClaimId ? { crossParentDiagnosticClaimId } : {}),
};
const nextSource = effectiveSource
? {