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:
7
.changeset/prevent-cross-parent-diagnostic-duplicates.md
Normal file
7
.changeset/prevent-cross-parent-diagnostic-duplicates.md
Normal 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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
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";
|
import type { TaskStore } from "../store.js";
|
||||||
|
|
||||||
describe("findSameAgentDuplicates", () => {
|
describe("findSameAgentDuplicates", () => {
|
||||||
@@ -122,6 +122,44 @@ describe("findSameAgentDuplicates", () => {
|
|||||||
expect(claim("Add new support")).toMatch(/^agent-parent-intent:FN-8277:[a-f0-9]{64}$/);
|
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", () => {
|
it("does not match sibling with different parent task", () => {
|
||||||
const matches = findSameAgentDuplicates(
|
const matches = findSameAgentDuplicates(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,6 +78,49 @@ export function computeParentIntentClaimId(input: SameAgentDuplicateInput): stri
|
|||||||
return parentId && anchor ? `agent-parent-intent:${parentId}:${anchor}` : null;
|
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.
|
* Find candidate tasks that look like duplicates spawned by the same caller.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -725,6 +725,8 @@ export type { TaskDependencyMutation } from "./store.js";
|
|||||||
export {
|
export {
|
||||||
findSameAgentDuplicates,
|
findSameAgentDuplicates,
|
||||||
computeParentIntentClaimId,
|
computeParentIntentClaimId,
|
||||||
|
computeCrossParentDiagnosticClaim,
|
||||||
|
computeCrossParentDiagnosticClaimId,
|
||||||
archiveAsSameAgentDuplicate,
|
archiveAsSameAgentDuplicate,
|
||||||
flagSameAgentDuplicate,
|
flagSameAgentDuplicate,
|
||||||
flagTriageDuplicate,
|
flagTriageDuplicate,
|
||||||
|
|||||||
@@ -768,6 +768,8 @@ export type { TaskDependencyMutation } from "./store.js";
|
|||||||
export {
|
export {
|
||||||
findSameAgentDuplicates,
|
findSameAgentDuplicates,
|
||||||
computeParentIntentClaimId,
|
computeParentIntentClaimId,
|
||||||
|
computeCrossParentDiagnosticClaim,
|
||||||
|
computeCrossParentDiagnosticClaimId,
|
||||||
archiveAsSameAgentDuplicate,
|
archiveAsSameAgentDuplicate,
|
||||||
flagSameAgentDuplicate,
|
flagSameAgentDuplicate,
|
||||||
flagTriageDuplicate,
|
flagTriageDuplicate,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
|
|||||||
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||||
return {
|
return {
|
||||||
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
|
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
|
||||||
|
getRootDir: vi.fn().mockReturnValue("/project"),
|
||||||
|
searchTasks: vi.fn().mockResolvedValue([]),
|
||||||
findRecentTasksBySourceParentTaskId: vi.fn().mockResolvedValue([]),
|
findRecentTasksBySourceParentTaskId: vi.fn().mockResolvedValue([]),
|
||||||
findRecentTasksByContentFingerprint: vi.fn().mockResolvedValue([]),
|
findRecentTasksByContentFingerprint: vi.fn().mockResolvedValue([]),
|
||||||
updateTask: vi.fn(),
|
updateTask: vi.fn(),
|
||||||
@@ -368,6 +370,92 @@ describe("createDelegateTaskTool", () => {
|
|||||||
expect(taskStore.moveTask).not.toHaveBeenCalled();
|
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 () => {
|
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" });
|
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);
|
await tool.execute("call-1", { description: "Capture optional report screenshots" }, undefined as any, undefined as any, undefined as any);
|
||||||
|
|||||||
@@ -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 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 { 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 { 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 { ResearchOrchestrator } from "./research-orchestrator.js";
|
||||||
import { ResearchProviderRegistry } from "./research/provider-registry.js";
|
import { ResearchProviderRegistry } from "./research/provider-registry.js";
|
||||||
import { ResearchStepRunner } from "./research-step-runner.js";
|
import { ResearchStepRunner } from "./research-step-runner.js";
|
||||||
@@ -975,6 +975,13 @@ export async function createAgentTask(
|
|||||||
const rootDir = options?.rootDir;
|
const rootDir = options?.rootDir;
|
||||||
const sourceParentTaskId = (input.source?.sourceParentTaskId ?? options?.sourceTaskId)?.trim().toUpperCase();
|
const sourceParentTaskId = (input.source?.sourceParentTaskId ?? options?.sourceTaskId)?.trim().toUpperCase();
|
||||||
const sourceAgentId = input.source?.sourceAgentId ?? options?.sourceAgentId;
|
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
|
const effectiveSource = input.source || sourceParentTaskId || sourceAgentId
|
||||||
? {
|
? {
|
||||||
sourceType: input.source?.sourceType ?? "api" as const,
|
sourceType: input.source?.sourceType ?? "api" as const,
|
||||||
@@ -987,11 +994,11 @@ export async function createAgentTask(
|
|||||||
title: input.title,
|
title: input.title,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
}, {
|
}, {
|
||||||
lockScope: rootDir ?? store.getRootDir?.() ?? "agent-tools",
|
lockScope: duplicateLockScope,
|
||||||
bypass: options?.bypassDuplicateCheck === true,
|
bypass: options?.bypassDuplicateCheck === true,
|
||||||
acknowledgedDuplicates: options?.acknowledgedDuplicates,
|
acknowledgedDuplicates: options?.acknowledgedDuplicates,
|
||||||
serializationKey: sourceParentTaskId ? `parent:${sourceParentTaskId}` : undefined,
|
serializationKey: crossParentDiagnosticClaimId ?? (sourceParentTaskId ? `parent:${sourceParentTaskId}` : undefined),
|
||||||
sourceParentTaskId,
|
sourceParentTaskId: crossParentDiagnosticClaimId ? null : sourceParentTaskId,
|
||||||
logger: log,
|
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) {
|
if (sourceParentTaskId && options?.bypassDuplicateCheck !== true) {
|
||||||
try {
|
try {
|
||||||
const acknowledged = new Set(options?.acknowledgedDuplicates ?? []);
|
const acknowledged = new Set(options?.acknowledgedDuplicates ?? []);
|
||||||
@@ -1037,6 +1073,7 @@ export async function createAgentTask(
|
|||||||
const sourceMetadata = {
|
const sourceMetadata = {
|
||||||
...(effectiveSource?.sourceMetadata ?? {}),
|
...(effectiveSource?.sourceMetadata ?? {}),
|
||||||
...(guard.fingerprint ? { contentFingerprint: guard.fingerprint } : {}),
|
...(guard.fingerprint ? { contentFingerprint: guard.fingerprint } : {}),
|
||||||
|
...(crossParentDiagnosticClaimId ? { crossParentDiagnosticClaimId } : {}),
|
||||||
};
|
};
|
||||||
const nextSource = effectiveSource
|
const nextSource = effectiveSource
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in New Issue
Block a user