FN-7658: gate same-agent duplicate auto-archiving behind opt-in setting
Duplicate tasks created by the same agent are no longer auto-archived by default; they are flagged for review instead, controlled by a new opt-in project setting. - Add project setting `autoArchiveDuplicateTasksEnabled` (default false) gating the FN-4892 same-agent duplicate intake path - Add `flagSameAgentDuplicate` path and `nearDuplicateOf` metadata used when auto-archive is disabled; tombstone-resurrection blocking is unchanged - Wire the setting through core settings schema/types/store, dashboard SchedulingSection UI, and i18n strings - Update docs (settings-reference.md, task-management.md) to describe the new default-off behavior - Add a changeset for the @runfusion/fusion minor release - Extend duplicate-intake, tombstone-window, store-parent-task-dedup, and reliability-interaction tests to cover both flag states Files changed: $(cat /tmp/fn7658_stat.txt) Fusion-Task-Id: FN-7658 Fusion-Task-Lineage: 7d0d1074-1020-48a8-b96f-186154c2c408 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7658-no-auto-archive-duplicates.md
Normal file
7
.changeset/fn-7658-no-auto-archive-duplicates.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Duplicate tasks are no longer auto-archived on creation by default — they are flagged for review instead.
|
||||
category: feature
|
||||
dev: Adds project setting `autoArchiveDuplicateTasksEnabled` (default false) gating the FN-4892 same-agent duplicate intake path in store `_maybeAutoArchiveSameAgentDuplicate`; disabled path uses new `flagSameAgentDuplicate` and sets `nearDuplicateOf` metadata. Tombstone-resurrection blocking is unchanged.
|
||||
@@ -601,6 +601,7 @@ Default notes:
|
||||
| `autoArchiveDoneTasksEnabled` | `boolean` | `true` | Enable periodic auto-archiving of done tasks. |
|
||||
| `autoArchiveDoneAfterMs` | `number` | `172800000` | Age in ms after entering done before auto-archive (48h). |
|
||||
| `doneAutoArchiveDays` | `number` | `0` | Integer day-based done-task retention. `0` disables day override; values `> 0` take precedence over `autoArchiveDoneAfterMs`. |
|
||||
| `autoArchiveDuplicateTasksEnabled` | `boolean` | `false` | FN-7658: gates whether same-agent duplicate intake (FN-4892) auto-archives the later task. Default `false` — the duplicate is flagged in place (`nearDuplicateOf`/`nearDuplicateScore` marker, yellow "Duplicate" chip with Keep/Archive actions) instead of being archived automatically. Set `true` to restore the pre-FN-7658 auto-archive behavior. Does not affect ghost-bug preflight or tombstone-resurrection blocking. |
|
||||
| `archiveAgentLogMode` | `"none" \| "compact" \| "full"` | `"compact"` | Agent log retention strategy for cold archive snapshots. |
|
||||
| `autoUpdatePrStatus` | `boolean` | `false` | Auto-refresh PR status badges. |
|
||||
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
|
||||
|
||||
@@ -149,14 +149,20 @@ The duplicate-close task log line remains `Duplicate of <canonicalTaskId> — cl
|
||||
Fusion applies two conservative intake heuristics that may auto-archive newly filed tasks before execution starts:
|
||||
|
||||
- **Ghost-bug preflight** (triage finalize path): for bug-fix-shaped specs that cite concrete constructs/commands, Fusion probes current `main`. If all definitive probes show the cited bug does not reproduce, the task is archived as `auto-resolved-ghost-bug`.
|
||||
- **Same-agent duplicate intake** (create path): if the same `source.sourceAgentId` filed a highly similar task within 24h (threshold `0.75`), the later task is archived as `auto-resolved-duplicate` and the earliest sibling is kept.
|
||||
- **Same-agent duplicate intake** (create path): if the same `source.sourceAgentId` (or `source.sourceParentTaskId`) filed a highly similar task within 24h (threshold `0.75`), Fusion still detects the near-duplicate — but what happens next depends on the `autoArchiveDuplicateTasksEnabled` project/global setting (default **`false`**, FN-7658):
|
||||
- **Default (`false`)**: the later task is left in place and flagged via the same near-duplicate marker used elsewhere (`sourceMetadata.nearDuplicateOf` / `nearDuplicateScore`), so the dashboard's yellow "Duplicate" chip with Keep/Archive actions surfaces it for a human decision. The task is never moved to `archived` automatically.
|
||||
- **`true`** (legacy behavior, opt-in): the later task is archived as `auto-resolved-duplicate` and the earliest sibling is kept, exactly as before FN-7658.
|
||||
|
||||
Ghost-bug preflight is unaffected by `autoArchiveDuplicateTasksEnabled` — it is a distinct heuristic and always auto-archives on a definitive non-repro.
|
||||
|
||||
Both heuristics are **fail-open**: probe/detection errors, timeouts, or inconclusive signals do not block normal intake — the task continues in the regular flow.
|
||||
|
||||
Tombstone-resurrection blocking (recreating a soft-deleted task within the sticky window) is a separate safety mechanism from same-agent duplicate intake and is **not** gated by `autoArchiveDuplicateTasksEnabled` — it always throws `TombstonedTaskResurrectionError` regardless of the setting.
|
||||
|
||||
Activity + run-audit event types:
|
||||
|
||||
- `task:auto-archived-ghost-bug`
|
||||
- `task:auto-archived-duplicate`
|
||||
- `task:auto-archived-duplicate` — emitted for both outcomes of the same-agent duplicate heuristic; the flag-only (default) path sets `metadata.source: "same-agent-flagged"` to distinguish it from the legacy auto-archive outcome.
|
||||
|
||||
These appear in task activity history; run-audit entries are emitted where run context exists (triage/engine paths). Store-only intake paths record activity without synthetic run context.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("FN-5233 tombstone sticky-window duplicate intake", () => {
|
||||
})).resolves.toMatchObject({ id: expect.any(String) });
|
||||
});
|
||||
|
||||
it("keeps live-task duplicate behavior (auto-archive) unchanged", async () => {
|
||||
it("FN-7658: flags (does not auto-archive) live-task duplicates by default", async () => {
|
||||
const store = harness.store();
|
||||
const live = await store.createTask({
|
||||
title: "Live dup",
|
||||
@@ -110,12 +110,48 @@ describe("FN-5233 tombstone sticky-window duplicate intake", () => {
|
||||
description: "duplicate text",
|
||||
source: { sourceType: "unknown", sourceAgentId: "agent-4" },
|
||||
});
|
||||
expect(dup.column).toBe("archived");
|
||||
expect(dup.column).not.toBe("archived");
|
||||
expect(dup.sourceMetadata?.nearDuplicateOf).toBe(live.id);
|
||||
const events = (store as any).db.prepare("SELECT mutationType FROM runAuditEvents WHERE mutationType = 'intake:resurrection-blocked'").all() as Array<{ mutationType: string }>;
|
||||
expect(events).toHaveLength(0);
|
||||
expect(live.id).not.toBe(dup.id);
|
||||
});
|
||||
|
||||
it("FN-7658: keeps legacy auto-archive behavior when autoArchiveDuplicateTasksEnabled is true", async () => {
|
||||
const store = harness.store();
|
||||
await store.updateSettings({ autoArchiveDuplicateTasksEnabled: true });
|
||||
const live = await store.createTask({
|
||||
title: "Live dup enabled",
|
||||
description: "duplicate text enabled",
|
||||
source: { sourceType: "unknown", sourceAgentId: "agent-4b" },
|
||||
});
|
||||
const dup = await store.createTask({
|
||||
title: "Live dup enabled",
|
||||
description: "duplicate text enabled",
|
||||
source: { sourceType: "unknown", sourceAgentId: "agent-4b" },
|
||||
});
|
||||
expect(dup.column).toBe("archived");
|
||||
expect(live.id).not.toBe(dup.id);
|
||||
});
|
||||
|
||||
it("FN-7658: tombstone-resurrection blocking still fires when autoArchiveDuplicateTasksEnabled is false", async () => {
|
||||
const store = harness.store();
|
||||
await store.updateSettings({ tombstoneStickyWindowDays: 7, autoArchiveDuplicateTasksEnabled: false });
|
||||
|
||||
const original = await store.createTask({
|
||||
title: "Resurrection guard stays on",
|
||||
description: "Fix resurrection guard regardless of duplicate archive setting",
|
||||
source: { sourceType: "unknown", sourceAgentId: "agent-4c" },
|
||||
});
|
||||
await store.deleteTask(original.id);
|
||||
|
||||
await expect(store.createTask({
|
||||
title: "Resurrection guard stays on",
|
||||
description: "Fix resurrection guard regardless of duplicate archive setting",
|
||||
source: { sourceType: "unknown", sourceAgentId: "agent-4c" },
|
||||
})).rejects.toBeInstanceOf(TombstonedTaskResurrectionError);
|
||||
});
|
||||
|
||||
it("fails open when tombstone widening query errors", async () => {
|
||||
const store = harness.store();
|
||||
const db = (store as any).db;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { findSameAgentDuplicates } from "../duplicate-intake.js";
|
||||
import { findSameAgentDuplicates, flagSameAgentDuplicate } from "../duplicate-intake.js";
|
||||
import type { TaskStore } from "../store.js";
|
||||
|
||||
describe("findSameAgentDuplicates", () => {
|
||||
const nowMs = Date.now();
|
||||
@@ -116,3 +117,52 @@ describe("findSameAgentDuplicates", () => {
|
||||
expect(matches[0]?.id).toBe("FN-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("flagSameAgentDuplicate (FN-7658)", () => {
|
||||
function createMockStore() {
|
||||
const logEntry = vi.fn().mockResolvedValue(undefined);
|
||||
const recordActivity = vi.fn().mockResolvedValue(undefined);
|
||||
const updateTask = vi.fn().mockResolvedValue(undefined);
|
||||
return {
|
||||
store: { logEntry, recordActivity, updateTask } as unknown as TaskStore,
|
||||
logEntry,
|
||||
recordActivity,
|
||||
updateTask,
|
||||
};
|
||||
}
|
||||
|
||||
it("logs, records a flag-only activity, and sets the near-duplicate marker without moving the task", async () => {
|
||||
const { store, logEntry, recordActivity, updateTask } = createMockStore();
|
||||
|
||||
await flagSameAgentDuplicate(store, "FN-2", ["FN-1"], { "FN-1": 0.9 });
|
||||
|
||||
expect(logEntry).toHaveBeenCalledTimes(1);
|
||||
expect(logEntry.mock.calls[0]?.[0]).toBe("FN-2");
|
||||
|
||||
expect(recordActivity).toHaveBeenCalledTimes(1);
|
||||
const activity = recordActivity.mock.calls[0]?.[0];
|
||||
expect(activity).toMatchObject({
|
||||
type: "task:auto-archived-duplicate",
|
||||
taskId: "FN-2",
|
||||
metadata: { siblingTaskIds: ["FN-1"], scores: { "FN-1": 0.9 }, source: "same-agent-flagged" },
|
||||
});
|
||||
|
||||
expect(updateTask).toHaveBeenCalledTimes(1);
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-2", {
|
||||
sourceMetadataPatch: { nearDuplicateOf: "FN-1", nearDuplicateScore: 0.9 },
|
||||
});
|
||||
|
||||
// Must NOT call moveTask — flagSameAgentDuplicate leaves the task's column alone.
|
||||
expect((store as unknown as { moveTask?: unknown }).moveTask).toBeUndefined();
|
||||
});
|
||||
|
||||
it("picks the first sibling id as the canonical near-duplicate marker", async () => {
|
||||
const { store, updateTask } = createMockStore();
|
||||
|
||||
await flagSameAgentDuplicate(store, "FN-3", ["FN-1", "FN-2"], { "FN-1": 0.8, "FN-2": 0.95 });
|
||||
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-3", {
|
||||
sourceMetadataPatch: { nearDuplicateOf: "FN-1", nearDuplicateScore: 0.8 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,10 @@ describe("TaskStore parent-task duplicate intake", () => {
|
||||
await harness.afterEach();
|
||||
});
|
||||
|
||||
it("auto-archives a sibling created by the same parent task with similar description", async () => {
|
||||
it("auto-archives a sibling created by the same parent task with similar description when autoArchiveDuplicateTasksEnabled is true", async () => {
|
||||
// FN-7658: default is now flag-in-place; explicitly opt back into the legacy
|
||||
// auto-archive behavior to cover it is still reachable.
|
||||
await store.updateSettings({ autoArchiveDuplicateTasksEnabled: true });
|
||||
const parentId = "FN-PARENT";
|
||||
|
||||
const first = await store.createTask({
|
||||
|
||||
@@ -108,3 +108,52 @@ export async function archiveAsSameAgentDuplicate(
|
||||
});
|
||||
await store.moveTask(taskId, "archived");
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:DuplicateIntake 2026-07-07-00:00 (FN-7658):
|
||||
* This is the DEFAULT same-agent duplicate intake outcome (the setting
|
||||
* `autoArchiveDuplicateTasksEnabled` defaults to `false`). Operators do not
|
||||
* want duplicates silently disappearing into `archived` on creation — they
|
||||
* want visibility and a chance to decide. Instead of moving the task, this
|
||||
* records the same near-duplicate marker (`nearDuplicateOf`/`nearDuplicateScore`)
|
||||
* used elsewhere (see FN-6439 `clearNearDuplicateReferencesTo`) so the dashboard's
|
||||
* existing yellow "Duplicate" chip with Keep/Archive actions to surface it
|
||||
* for a human decision. The task is left in whatever column it was created
|
||||
* in — this function does NOT call `moveTask`.
|
||||
*
|
||||
* `siblingIds` should be ordered with the canonical/earliest sibling first;
|
||||
* that first id becomes `nearDuplicateOf` and its score becomes
|
||||
* `nearDuplicateScore`.
|
||||
*/
|
||||
export async function flagSameAgentDuplicate(
|
||||
store: TaskStore,
|
||||
taskId: string,
|
||||
siblingIds: string[],
|
||||
scores: Record<string, number>,
|
||||
): Promise<Record<string, unknown> | undefined> {
|
||||
const canonicalId = siblingIds[0];
|
||||
await store.logEntry(
|
||||
taskId,
|
||||
"Flagged as same-agent duplicate",
|
||||
`Near-duplicate of recently-filed sibling task(s): ${siblingIds.join(", ")} (not archived — autoArchiveDuplicateTasksEnabled is off)`,
|
||||
);
|
||||
// FN-7658: reuse the existing duplicate activity type with a `source` disambiguator
|
||||
// rather than inventing a schema-unknown activity type; run-audit consumers already
|
||||
// understand `task:auto-archived-duplicate` and can key off `metadata.source`.
|
||||
await store.recordActivity({
|
||||
type: "task:auto-archived-duplicate",
|
||||
taskId,
|
||||
details: "Flagged (not archived) as same-agent duplicate during intake",
|
||||
metadata: { siblingTaskIds: siblingIds, scores, source: "same-agent-flagged" },
|
||||
});
|
||||
if (!canonicalId) return undefined;
|
||||
const sourceMetadataPatch = {
|
||||
nearDuplicateOf: canonicalId,
|
||||
nearDuplicateScore: scores[canonicalId] ?? null,
|
||||
};
|
||||
await store.updateTask(taskId, { sourceMetadataPatch });
|
||||
// Return the applied patch so the in-memory task object held by the createTask
|
||||
// caller (which was written to disk BEFORE this flag runs) can be kept in sync
|
||||
// without a redundant re-fetch.
|
||||
return sourceMetadataPatch;
|
||||
}
|
||||
|
||||
@@ -639,6 +639,7 @@ export type { TaskDependencyMutation } from "./store.js";
|
||||
export {
|
||||
findSameAgentDuplicates,
|
||||
archiveAsSameAgentDuplicate,
|
||||
flagSameAgentDuplicate,
|
||||
type SameAgentDuplicateInput,
|
||||
type SameAgentDuplicateCandidate,
|
||||
type SameAgentDuplicateMatch,
|
||||
|
||||
@@ -518,6 +518,10 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
autoArchiveDoneTasksEnabled: true,
|
||||
autoArchiveDoneAfterMs: 48 * 60 * 60 * 1000,
|
||||
doneAutoArchiveDays: 0,
|
||||
// FNXC:DuplicateIntake 2026-07-07-00:00 (FN-7658): default OFF — operators
|
||||
// decide via the near-duplicate flag/UI instead of tasks silently vanishing
|
||||
// into `archived` during intake. Set true to restore the pre-FN-7658 behavior.
|
||||
autoArchiveDuplicateTasksEnabled: false,
|
||||
archiveAgentLogMode: "compact",
|
||||
autoUpdatePrStatus: false,
|
||||
githubCommentOnDone: false,
|
||||
|
||||
@@ -202,7 +202,7 @@ import {
|
||||
} from "./distributed-task-id.js";
|
||||
import { detectStalledReview } from "./stalled-review-detector.js";
|
||||
import { computeRetrySummary } from "./retry-summary.js";
|
||||
import { archiveAsSameAgentDuplicate, findSameAgentDuplicates } from "./duplicate-intake.js";
|
||||
import { archiveAsSameAgentDuplicate, flagSameAgentDuplicate, findSameAgentDuplicates } from "./duplicate-intake.js";
|
||||
import { isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js";
|
||||
import {
|
||||
detectTaskIdIntegrityAnomalies,
|
||||
@@ -5252,8 +5252,25 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
const siblingTaskIds = matches.filter((match) => !match.tombstoned).map((match) => match.id);
|
||||
if (siblingTaskIds.length === 0) return;
|
||||
const scores = Object.fromEntries(matches.filter((match) => !match.tombstoned).map((match) => [match.id, match.score]));
|
||||
await archiveAsSameAgentDuplicate(this, task.id, siblingTaskIds, scores);
|
||||
task.column = "archived";
|
||||
/*
|
||||
FNXC:DuplicateIntake 2026-07-07-00:00 (FN-7658):
|
||||
Operators do not want same-agent duplicates silently vanishing into `archived`
|
||||
during intake. Default (`autoArchiveDuplicateTasksEnabled` falsey) flags the
|
||||
duplicate in place via the near-duplicate marker so a human decides (Keep/Archive
|
||||
chip). Only an explicit `true` restores the pre-FN-7658 auto-archive behavior.
|
||||
NOTE: the tombstone-resurrection block above (`TombstonedTaskResurrectionError`)
|
||||
is a distinct safety mechanism and is intentionally NOT gated by this setting —
|
||||
it always fires regardless of `autoArchiveDuplicateTasksEnabled`.
|
||||
*/
|
||||
if (settings.autoArchiveDuplicateTasksEnabled === true) {
|
||||
await archiveAsSameAgentDuplicate(this, task.id, siblingTaskIds, scores);
|
||||
task.column = "archived";
|
||||
} else {
|
||||
const appliedPatch = await flagSameAgentDuplicate(this, task.id, siblingTaskIds, scores);
|
||||
if (appliedPatch) {
|
||||
task.sourceMetadata = { ...(task.sourceMetadata ?? {}), ...appliedPatch };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TombstonedTaskResurrectionError) {
|
||||
throw error;
|
||||
|
||||
@@ -4423,6 +4423,17 @@ export interface ProjectSettings {
|
||||
/** Retention in integer days before done tasks are auto-archived.
|
||||
* 0 disables this days-based override. When > 0, takes precedence over autoArchiveDoneAfterMs. */
|
||||
doneAutoArchiveDays?: number;
|
||||
/**
|
||||
* FNXC:DuplicateIntake 2026-07-07-00:00 (FN-7658):
|
||||
* Operators do not want same-agent duplicate tasks silently archived on
|
||||
* creation (FN-4892 intake heuristic) — they want visibility and a chance
|
||||
* to decide. When `true`, `_maybeAutoArchiveSameAgentDuplicate` archives the
|
||||
* later task as before. When `false` (the default), the heuristic still
|
||||
* detects the duplicate but flags it in place via the existing near-duplicate
|
||||
* marker (`nearDuplicateOf`/`nearDuplicateScore`) instead of moving it to
|
||||
* `archived`, so the dashboard's yellow "Duplicate" chip with Keep/Archive
|
||||
* actions surfaces it for a human decision. Default: false. */
|
||||
autoArchiveDuplicateTasksEnabled?: boolean;
|
||||
/** How much agent log content to preserve when a task is moved to cold archive storage.
|
||||
* - "compact": deterministic summary plus a small recent-entry snapshot (default)
|
||||
* - "full": copy the full agent.log into archive.db
|
||||
|
||||
@@ -180,6 +180,21 @@ export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurr
|
||||
</select>
|
||||
<small>{t("settings.scheduling.compactModeKeepsArchiveSizeLowWhilePreserving", "Compact mode keeps archive size low while preserving recent agent activity for context. Default: compact.")}</small>
|
||||
</div>
|
||||
{/**
|
||||
* FNXC:DuplicateIntake 2026-07-07-00:00 (FN-7658):
|
||||
* Operators do not want same-agent duplicate tasks (FN-4892 intake heuristic)
|
||||
* silently archived on creation — they want visibility and a chance to decide
|
||||
* via the near-duplicate flag/UI. Default off; this toggle restores the old
|
||||
* aggressive auto-archive behavior when enabled.
|
||||
*/}
|
||||
<div className="form-group">
|
||||
<label htmlFor="autoArchiveDuplicateTasksEnabled" className="checkbox-label">
|
||||
<input id="autoArchiveDuplicateTasksEnabled" type="checkbox" checked={form.autoArchiveDuplicateTasksEnabled ?? false} onChange={(e) => setForm((f) => ({
|
||||
...f,
|
||||
autoArchiveDuplicateTasksEnabled: e.target.checked,
|
||||
}))}/>{t("settings.scheduling.autoArchiveDuplicateTasks", " Automatically archive duplicate tasks ")}</label>
|
||||
<small>{t("settings.scheduling.autoArchiveDuplicateTasksHelp", "Automatically archive tasks detected as same-agent duplicates on creation (off by default). When disabled, duplicates are flagged in place with the yellow Duplicate chip and Keep/Archive actions instead of being archived automatically.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="maxStuckKills">{t("settings.scheduling.maxStuckRetries", "Max Stuck Retries")}</label>
|
||||
<input id="maxStuckKills" type="number" min={1} step={1} value={form.maxStuckKills ?? ""} onChange={(e) => {
|
||||
|
||||
@@ -202,6 +202,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
||||
autoArchiveDoneTasksEnabled: "scheduling.completedTasksOlderThanTheThresholdAreMoved",
|
||||
autoArchiveDoneAfterMs: "scheduling.numberOfDaysATaskCanStayIn",
|
||||
archiveAgentLogMode: "scheduling.compactModeKeepsArchiveSizeLowWhilePreserving",
|
||||
autoArchiveDuplicateTasksEnabled: "scheduling.autoArchiveDuplicateTasksHelp",
|
||||
maxStuckKills: "scheduling.maximumStuckDetectorRetriesBeforeATaskIs",
|
||||
groupOverlappingFiles: "scheduling.whenEnabledTasksThatModifyTheSameFiles",
|
||||
ignoreHiddenOverlapPaths: "scheduling.ignoreHiddenDotPathsHelp",
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("reliability interactions: same-agent duplicate intake", () => {
|
||||
while (fixtures.length) await fixtures.pop()!.cleanup();
|
||||
});
|
||||
|
||||
it("archives later near-duplicate from same agent", async () => {
|
||||
it("FN-7658: flags (does not auto-archive) later near-duplicate from same agent by default", async () => {
|
||||
const fx = await createStore();
|
||||
fixtures.push(fx);
|
||||
|
||||
@@ -40,6 +40,33 @@ describe("reliability interactions: same-agent duplicate intake", () => {
|
||||
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
expect((await fx.store.getTask(a.id)).column).toBe("triage");
|
||||
const refreshedB = await fx.store.getTask(b.id);
|
||||
expect(refreshedB.column).not.toBe("archived");
|
||||
expect(refreshedB.sourceMetadata?.nearDuplicateOf).toBe(a.id);
|
||||
const activity = await fx.store.getActivityLog({ type: "task:auto-archived-duplicate", limit: 10 });
|
||||
const entry = activity.find((item) => item.taskId === b.id);
|
||||
expect(entry).toBeTruthy();
|
||||
expect((entry?.metadata as { siblingTaskIds?: string[]; source?: string } | null)?.siblingTaskIds).toEqual([a.id]);
|
||||
expect((entry?.metadata as { source?: string } | null)?.source).toBe("same-agent-flagged");
|
||||
});
|
||||
|
||||
it("archives later near-duplicate from same agent when autoArchiveDuplicateTasksEnabled is true", async () => {
|
||||
const fx = await createStore();
|
||||
fixtures.push(fx);
|
||||
await fx.store.updateSettings({ autoArchiveDuplicateTasksEnabled: true });
|
||||
|
||||
const a = await fx.store.createTask({
|
||||
title: "fix: secrets sync typecheck",
|
||||
description: "typecheck error in secrets-sync",
|
||||
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-x" },
|
||||
});
|
||||
const b = await fx.store.createTask({
|
||||
title: "fix: secrets sync typecheck regression",
|
||||
description: "typecheck error in secrets-sync",
|
||||
source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-x" },
|
||||
});
|
||||
|
||||
expect((await fx.store.getTask(a.id)).column).toBe("triage");
|
||||
expect((await fx.store.getTask(b.id)).column).toBe("archived");
|
||||
const activity = await fx.store.getActivityLog({ type: "task:auto-archived-duplicate", limit: 10 });
|
||||
|
||||
@@ -6546,6 +6546,8 @@
|
||||
"addIgnoredPath": " Add ignored path ",
|
||||
"archiveAgentLog": "Archive Agent Log",
|
||||
"archiveCompletedTasksAfterDays": "Archive Completed Tasks After (days)",
|
||||
"autoArchiveDuplicateTasks": " Automatically archive duplicate tasks ",
|
||||
"autoArchiveDuplicateTasksHelp": "Automatically archive tasks detected as same-agent duplicates on creation (off by default). When disabled, duplicates are flagged in place with the yellow Duplicate chip and Keep/Archive actions instead of being archived automatically.",
|
||||
"backlogNoTaskAutoClaimIsExecutorOnly": "Backlog/no-task auto-claim is executor-only by default. Enable to let engineer-role agents auto-claim unowned backlog tasks; explicit routing and delegation are unchanged. Default: off.",
|
||||
"browse": " Browse ",
|
||||
"closeParenPeriod": ").",
|
||||
|
||||
Reference in New Issue
Block a user