fix(core): converge cross-parent duplicate follow-ups naming the same failing file; surface task provenance in Stats tab

Widen computeCrossParentDiagnosticClaim so repair tasks phrased as
'exceeds limit / oversized / blocking X / so X passes' converge on a
file-path or distinctive-slug anchor at creation time (FN-8510/8511/
8513/8514 incident: four executors on unrelated parents filed the same
oversized-changeset follow-up and none deduped before triage).

Add a Provenance section to the Task Detail Stats tab showing source
type, parent task, creating agent, imported-issue link, and the triage
near-duplicate marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-22 19:10:06 -07:00
parent fd9e4b2a30
commit 085f7b99c8
7 changed files with 214 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Duplicate follow-up tasks naming the same failing file now converge at creation across parent tasks.
category: fix
dev: `computeCrossParentDiagnosticClaim` gains file-path/slug fallback objects and wider action/failure gates (exceeds, oversized, blocks, "so X passes"); FN-8510/8511/8513/8514 incident.

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Task Stats tab now shows creation provenance — source type, parent task, creating agent, and duplicate flags.
category: feature
dev: New Provenance section in `TaskTokenStatsPanel` reading the task's flat source fields and `sourceMetadata.nearDuplicateOf`/`issueUrl`.

View File

@@ -138,6 +138,57 @@ describe("findSameAgentDuplicates", () => {
expect(claims[0]).toMatch(/^agent-diagnostic-intent:/); expect(claims[0]).toMatch(/^agent-diagnostic-intent:/);
}); });
/*
FNXC:TaskCreationDeduplication 2026-07-22-14:30:
FN-8510/8511/8513/8514 regression: four executors on unrelated parents filed the same
"fix the oversized changeset summary" follow-up with different phrasings (exceeds limit /
so check:changesets passes / oversized / blocking) and different fingerprints; all four
must converge on one cross-parent claim anchored to the named changeset file.
*/
it("derives one cross-parent claim for a gate failure named by file path or slug (FN-8514)", () => {
const incidents = [
{
title: "Shorten mobile board changeset summary",
description: "Fix the pre-existing `.changeset/mobile-board-pointercancel-settle.md` summary exceeding the 120-character changeset-format limit, so `pnpm check:changesets` passes.",
},
{
description: "Shorten `.changeset/mobile-board-pointercancel-settle.md` summary to <=120 chars so `pnpm check:changesets` passes. Existing summary is 131 chars; unrelated to FN-8503.",
},
{
description: "Fix oversized summary in existing mobile-board-pointercancel-settle changeset so pnpm check:changesets passes.",
},
{
description: "Fix existing changeset format failure: .changeset/mobile-board-pointercancel-settle.md summary exceeds 120-character limit, blocking pnpm check:changesets.",
},
];
const claims = incidents.map((input) => computeCrossParentDiagnosticClaimId(input));
expect(new Set(claims).size).toBe(1);
expect(claims[0]).toMatch(/^agent-diagnostic-intent:/);
});
it("converges failure paraphrases naming the same file path without a distinctive slug", () => {
const first = computeCrossParentDiagnosticClaimId({
description: "Fix broken import in packages/core/src/store.ts causing a typecheck error.",
});
const second = computeCrossParentDiagnosticClaimId({
description: "Investigate packages/core/src/store.ts typecheck failure observed during pnpm verify:fast.",
});
expect(first).not.toBeNull();
expect(first).toBe(second);
});
it("does not claim ordinary work that merely names a file path", () => {
expect(computeCrossParentDiagnosticClaimId({
description: "Add caching to packages/core/src/store.ts for faster board loads",
})).toBeNull();
expect(computeCrossParentDiagnosticClaimId({
description: "Shorten the onboarding copy in WelcomeModal",
})).toBeNull();
});
it("does not globally claim ordinary work or unrelated work on the same module", () => { it("does not globally claim ordinary work or unrelated work on the same module", () => {
expect(computeCrossParentDiagnosticClaimId({ expect(computeCrossParentDiagnosticClaimId({
description: "Add screenshot upload support using html2canvas", description: "Add screenshot upload support using html2canvas",

View File

@@ -78,12 +78,37 @@ 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; FNXC:TaskCreationDeduplication 2026-07-22-14:30:
Widened after the FN-8510/8511/8513/8514 incident: four executors on unrelated parent tasks
each filed "fix the oversized .changeset/mobile-board-pointercancel-settle.md summary" follow-ups,
and none converged at creation time because the failure was phrased as "exceeds the limit /
blocking pnpm check:changesets / so check:changesets passes" with a file path — not as the
"missing/unresolved <module>" shape the original diagnostic patterns required.
The action/failure gates now cover remediation verbs (shorten, correct, unblock) and
gate-failure idioms (exceeds, oversized, blocks/blocking, "so <command> passes").
"blocked" is deliberately excluded: "blocked by FN-XXXX" is dependency prose, not a failure report.
*/
const DIAGNOSTIC_ACTION_PATTERN = /\b(?:correct|fix|investigate|repair|resolve|restore|shorten|unblock)\b/i;
const DIAGNOSTIC_FAILURE_PATTERN = /\b(?:cannot|can't|error|exceed(?:s|ed|ing)?|fail(?:ed|ure|s)?|missing|oversized|block(?:s|ing)|ts\d{4}|typecheck|unresolved)\b|\bso\b[^.\n]*\bpass(?:es)?\b/i;
const DIAGNOSTIC_OBJECT_PATTERNS = [ const DIAGNOSTIC_OBJECT_PATTERNS = [
/\b(?:missing|unresolved)\s+([`'"]?[@a-z0-9][@a-z0-9._/-]*[`'"]?)/gi, /\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, /\b(?:cannot|can't)\s+(?:find|resolve)(?:\s+module)?\s+(?:the\s+)?([`'"]?[@a-z0-9][@a-z0-9._/-]*[`'"]?)/gi,
]; ];
/*
FNXC:TaskCreationDeduplication 2026-07-22-14:30:
Fallback objects for failure-shaped text with no missing/unresolved-style object.
A file path (segments joined by "/" with an extension) is a safe global convergence anchor:
two repair tasks naming the same file within the 24h window are the same follow-up.
Path objects normalize to their basename stem when the stem is a distinctive multi-hyphen slug
(>= 3 hyphen-separated segments), so ".changeset/foo-bar-baz.md" and prose that names only
"foo-bar-baz" converge; generic stems like "index" keep the full path so
"packages/alpha/index.ts" and "packages/beta/index.ts" stay distinct.
Bare multi-hyphen slugs are extracted for the same reason (FN-8513 named the changeset only
by its slug, never by path).
*/
const DIAGNOSTIC_PATH_PATTERN = /(?:^|[\s`'"(])(\.?[a-z0-9_@-]+(?:\/[a-z0-9._@-]+)+\.[a-z0-9]+)/gim;
const DIAGNOSTIC_SLUG_PATTERN = /\b([a-z0-9]+(?:-[a-z0-9]+){2,})\b/gi;
const IGNORED_DIAGNOSTIC_OBJECTS = new Set(["a", "an", "dependency", "module", "the", "type", "types"]); const IGNORED_DIAGNOSTIC_OBJECTS = new Set(["a", "an", "dependency", "module", "the", "type", "types"]);
function normalizeDiagnosticObject(value: string): string | null { function normalizeDiagnosticObject(value: string): string | null {
@@ -96,6 +121,17 @@ function normalizeDiagnosticObject(value: string): string | null {
return /[0-9@./_-]/.test(normalized) ? normalized : null; return /[0-9@./_-]/.test(normalized) ? normalized : null;
} }
function isDistinctiveSlug(value: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+){2,}$/.test(value);
}
function normalizeDiagnosticPath(value: string): string {
const path = value.toLowerCase().replace(/^[`'".]+|[`'".,;:)]+$/g, "");
const basename = path.split("/").at(-1) ?? path;
const stem = basename.replace(/\.[a-z0-9]+$/, "");
return isDistinctiveSlug(stem) ? stem : path;
}
/** /**
* Stable claim for an active diagnostic follow-up that may be discovered by * Stable claim for an active diagnostic follow-up that may be discovered by
* several unrelated parent tasks. This is intentionally narrower than general * several unrelated parent tasks. This is intentionally narrower than general
@@ -111,7 +147,21 @@ export function computeCrossParentDiagnosticClaim(input: Pick<SameAgentDuplicate
.map((match) => normalizeDiagnosticObject(match[1] ?? "")) .map((match) => normalizeDiagnosticObject(match[1] ?? ""))
.filter((value): value is string => value !== null), .filter((value): value is string => value !== null),
); );
const diagnosticObject = [...new Set(objects)].sort()[0]; /*
FNXC:TaskCreationDeduplication 2026-07-22-14:30:
Primary missing/unresolved objects take precedence over path/slug fallbacks so that a text
naming both (e.g. "app/utils/capture-screenshot.ts imports unresolved html2canvas") keeps
converging on the precise object regardless of how the paraphrase spells the path.
*/
if (objects.length === 0) {
objects.push(
...[...text.matchAll(DIAGNOSTIC_PATH_PATTERN)].map((match) => normalizeDiagnosticPath(match[1] ?? "")),
...[...text.matchAll(DIAGNOSTIC_SLUG_PATTERN)]
.map((match) => (match[1] ?? "").toLowerCase())
.filter(isDistinctiveSlug),
);
}
const diagnosticObject = [...new Set(objects.filter(Boolean))].sort()[0];
if (!diagnosticObject) return null; if (!diagnosticObject) return null;
const fingerprint = computeContentFingerprint({ title: "agent-diagnostic-intent", description: diagnosticObject }); const fingerprint = computeContentFingerprint({ title: "agent-diagnostic-intent", description: diagnosticObject });
return fingerprint ? { id: `agent-diagnostic-intent:${fingerprint}`, searchTerm: diagnosticObject } : null; return fingerprint ? { id: `agent-diagnostic-intent:${fingerprint}`, searchTerm: diagnosticObject } : null;

View File

@@ -36,6 +36,10 @@ interface TaskTokenStatsPanelProps {
| "planningStartedAt" | "planningStartedAt"
| "column" | "column"
| "columnMovedAt" | "columnMovedAt"
| "sourceType"
| "sourceAgentId"
| "sourceParentTaskId"
| "sourceMetadata"
>; >;
} }
@@ -268,6 +272,60 @@ export function TaskTokenStatsPanel({ tokenUsage, loading, task }: TaskTokenStat
</dl> </dl>
</div> </div>
{/*
FNXC:TaskStatsProvenance 2026-07-22-14:45:
Operators triaging duplicate follow-up tasks (FN-8510/8511/8513/8514 incident) need to see
WHO filed a task without querying the DB: the Stats sub-tab surfaces creation provenance —
source type, the parent task whose executor called fn_task_create, the creating agent, and
the triage near-duplicate marker (canonical task id) when one was recorded.
Rows with no value are omitted rather than rendered empty.
*/}
{task?.sourceType ? (
<div className="task-token-stats-panel__section">
{/*
FNXC:TaskStatsProvenance 2026-07-22-14:45:
`taskDetail.provenance` is a nested locale OBJECT (Summary-tab provenance labels);
calling t() on it returns an object and crashes the render (issue #1863 pattern).
All keys here must be leaves under that namespace, e.g. `taskDetail.provenance.title`.
*/}
<h5>{t("taskDetail.provenance.title", "Provenance")}</h5>
<dl className="task-token-stats-panel__details">
<div className="task-token-stats-panel__detail-row">
<dt>{t("taskDetail.provenance.createdVia", "Created via")}</dt>
<dd>{task.sourceType}</dd>
</div>
{task.sourceParentTaskId ? (
<div className="task-token-stats-panel__detail-row">
<dt>{t("taskDetail.provenance.parentTask", "Parent task")}</dt>
<dd>{task.sourceParentTaskId}</dd>
</div>
) : null}
{task.sourceAgentId ? (
<div className="task-token-stats-panel__detail-row">
<dt>{t("taskDetail.provenance.creatingAgent", "Creating agent")}</dt>
<dd>{task.sourceAgentId}</dd>
</div>
) : null}
{typeof task.sourceMetadata?.issueUrl === "string" ? (
<div className="task-token-stats-panel__detail-row">
<dt>{t("taskDetail.provenance.importedFrom", "Imported from")}</dt>
<dd>
<a href={task.sourceMetadata.issueUrl} target="_blank" rel="noreferrer">
{task.sourceMetadata.issueUrl}
</a>
</dd>
</div>
) : null}
{typeof task.sourceMetadata?.nearDuplicateOf === "string" ? (
<div className="task-token-stats-panel__detail-row">
<dt>{t("taskDetail.provenance.nearDuplicateOf", "Flagged near-duplicate of")}</dt>
<dd>{task.sourceMetadata.nearDuplicateOf}</dd>
</div>
) : null}
</dl>
</div>
) : null}
<div className="task-token-stats-panel__section"> <div className="task-token-stats-panel__section">
<h5>{t("taskDetail.tokenUsage", "Token Usage")}</h5> <h5>{t("taskDetail.tokenUsage", "Token Usage")}</h5>
{!tokenUsage && loading ? ( {!tokenUsage && loading ? (

View File

@@ -285,6 +285,38 @@ describe("TaskTokenStatsPanel", () => {
* options, and assert the Execution Details label resolves to the leaf string * options, and assert the Execution Details label resolves to the leaf string
* without i18next's "returned an object instead of string" fallback. * without i18next's "returned an object instead of string" fallback.
*/ */
/*
FNXC:TaskStatsProvenance 2026-07-22-14:45:
Duplicate triage (FN-8510/8511/8513/8514) needs task provenance visible in the UI:
the Stats tab must show source type, the parent task whose executor filed this task,
the creating agent, and the triage near-duplicate marker when recorded.
*/
it("renders creation provenance including parent task, agent, and near-duplicate marker", () => {
render(<TaskTokenStatsPanel loading={false} tokenUsage={undefined} task={makeTask({
sourceType: "api",
sourceParentTaskId: "FN-8504",
sourceAgentId: "executor-9",
sourceMetadata: { nearDuplicateOf: "FN-8510", issueUrl: "https://github.com/Runfusion/Fusion/issues/2356" },
})} />);
expect(screen.getByText("Provenance")).toBeInTheDocument();
expect(screen.getByText("api")).toBeInTheDocument();
expect(screen.getByText("Parent task")).toBeInTheDocument();
expect(screen.getByText("FN-8504")).toBeInTheDocument();
expect(screen.getByText("Creating agent")).toBeInTheDocument();
expect(screen.getByText("executor-9")).toBeInTheDocument();
expect(screen.getByText("Flagged near-duplicate of")).toBeInTheDocument();
expect(screen.getByText("FN-8510")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "https://github.com/Runfusion/Fusion/issues/2356" }))
.toHaveAttribute("href", "https://github.com/Runfusion/Fusion/issues/2356");
});
it("omits the provenance section when the task has no recorded source", () => {
render(<TaskTokenStatsPanel loading={false} tokenUsage={undefined} task={makeTask()} />);
expect(screen.queryByText("Provenance")).toBeNull();
});
it("renders the execution-mode label against the real locale bundle without an object-key crash", async () => { it("renders the execution-mode label against the real locale bundle without an object-key crash", async () => {
const instance = createInstance(); const instance = createInstance();
await instance.use(initReactI18next).init({ await instance.use(initReactI18next).init({

View File

@@ -7861,9 +7861,14 @@
"provenance": { "provenance": {
"createdBy": "Created by", "createdBy": "Created by",
"createdVia": "Created via", "createdVia": "Created via",
"creatingAgent": "Creating agent",
"importedFrom": "Imported from",
"nearDuplicateOf": "Flagged near-duplicate of",
"parentTask": "Parent task",
"parentTaskOf": "of", "parentTaskOf": "of",
"createdToUndo": "Created to undo", "createdToUndo": "Created to undo",
"undoTask": "Undo task" "undoTask": "Undo task",
"title": "Provenance"
}, },
"recoveryState": "Recovery state", "recoveryState": "Recovery state",
"refine": { "refine": {