fix: stop inactive-duplicate clear from replan-storming
Clearing a DUPLICATE marker for an inactive or kept canonical left status:null without PROMPT.md. The scheduler treats planning→null as "finished planning" and re-dispatched the card; FS validation rebounded to needs-replan with no ceiling, and triage re-planned with empty feedback so the planner could re-emit the same inactive marker forever (FN-8704 / FN-8676). Leave needs-replan + dismissal metadata + replan feedback instead, share the clear contract across triage and self-healing, and bound scheduler filesystem-validation rebounds with the shared recovery budget.
This commit is contained in:
@@ -107,8 +107,18 @@ const canRun = hasGit && hasPg;
|
||||
const updated = await fx.store.getTask(duplicate.id);
|
||||
expect(updated.paused).not.toBe(true);
|
||||
expect(updated.pausedReason ?? null).toBeNull();
|
||||
expect(updated.status ?? null).toBeNull();
|
||||
// needs-replan (not null) so the card cannot look planning-finished without PROMPT
|
||||
expect(updated.status).toBe("needs-replan");
|
||||
expect(updated.sourceMetadata).toEqual(expect.objectContaining({
|
||||
nearDuplicateOf: canonicalId,
|
||||
nearDuplicateDismissed: true,
|
||||
duplicateSource: "triage-marker",
|
||||
}));
|
||||
expect(existsSync(promptPath)).toBe(false);
|
||||
expect(updated.log?.some((entry) =>
|
||||
entry.action === "Duplicate marker cleared for re-specification"
|
||||
&& String(entry.outcome ?? "").includes(canonicalId),
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -146,7 +156,11 @@ const canRun = hasGit && hasPg;
|
||||
const updated = await fx.store.getTask(duplicate.id);
|
||||
expect(updated.paused).not.toBe(true);
|
||||
expect(updated.pausedReason ?? null).toBeNull();
|
||||
expect(updated.sourceMetadata).toEqual(expect.objectContaining({ nearDuplicateOf: canonical.id.toLowerCase(), nearDuplicateDismissed: true }));
|
||||
expect(updated.status).toBe("needs-replan");
|
||||
expect(updated.sourceMetadata).toEqual(expect.objectContaining({
|
||||
nearDuplicateOf: canonical.id,
|
||||
nearDuplicateDismissed: true,
|
||||
}));
|
||||
expect(existsSync(promptPath)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ describe("FN-8356: reconcile stale duplicate-decision pauses", () => {
|
||||
const recovered = await store.getTask(id);
|
||||
expect(recovered?.paused).toBe(false);
|
||||
expect(recovered?.pausedReason).toBeNull();
|
||||
// needs-replan (not null) so the card cannot look planning-finished without a real PROMPT
|
||||
expect(recovered?.status).toBe("needs-replan");
|
||||
expect(recovered?.sourceMetadata?.nearDuplicateDismissed).toBe(true);
|
||||
// TaskCard and NotificationService both key their decision affordance on this predicate.
|
||||
expect(recovered?.pausedReason === "duplicate-decision-required").toBe(false);
|
||||
@@ -137,6 +139,7 @@ describe("FN-8356: reconcile stale duplicate-decision pauses", () => {
|
||||
const recovered = await store.getTask("FN-1");
|
||||
expect(recovered?.paused).toBe(false);
|
||||
expect(recovered?.pausedReason).toBeNull();
|
||||
expect(recovered?.status).toBe("needs-replan");
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -94,7 +94,18 @@ describe("triage explicit duplicate marker short-circuit", () => {
|
||||
});
|
||||
await expect(runExplicitDuplicateMarker(store, task, "DUPLICATE: FN-001\n", { ...settings, triageDuplicateResolution: "keep" })).resolves.toBe(true);
|
||||
expect(store.deleteTask).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-002", expect.objectContaining({ paused: false, pausedReason: null, status: null }));
|
||||
// Must leave needs-replan (not status:null) so the scheduler does not wake on a prompt-less card.
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-002", expect.objectContaining({
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: "needs-replan",
|
||||
sourceMetadataPatch: expect.objectContaining({ nearDuplicateOf: "FN-001", nearDuplicateDismissed: true }),
|
||||
}));
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-002",
|
||||
"Duplicate marker cleared for re-specification",
|
||||
expect.stringContaining("FN-001"),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not re-pause a same-canonical Keep acknowledgement after marker reprocessing", async () => {
|
||||
@@ -113,7 +124,8 @@ describe("triage explicit duplicate marker short-circuit", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-002", expect.objectContaining({
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
sourceMetadataPatch: { nearDuplicateDismissed: true },
|
||||
status: "needs-replan",
|
||||
sourceMetadataPatch: expect.objectContaining({ nearDuplicateOf: "FN-001", nearDuplicateDismissed: true }),
|
||||
}));
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-002", expect.objectContaining({ paused: true }));
|
||||
});
|
||||
@@ -165,11 +177,22 @@ describe("triage explicit duplicate marker short-circuit", () => {
|
||||
|
||||
await expect(runExplicitDuplicateMarker(store, task, "DUPLICATE: FN-001\n")).resolves.toBe(true);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-002", {
|
||||
// needs-replan + dismissal + feedback — never status:null (FN-8704 replan storm)
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-002", expect.objectContaining({
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: null,
|
||||
});
|
||||
status: "needs-replan",
|
||||
sourceMetadataPatch: expect.objectContaining({
|
||||
nearDuplicateOf: "FN-001",
|
||||
nearDuplicateDismissed: true,
|
||||
duplicateSource: "triage-marker",
|
||||
}),
|
||||
}));
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-002",
|
||||
"Duplicate marker cleared for re-specification",
|
||||
expect.stringMatching(/FN-001.*do not re-emit/i),
|
||||
);
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-002", expect.objectContaining({ paused: true }));
|
||||
expect(store.deleteTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
47
packages/engine/src/duplicate-marker-clear.ts
Normal file
47
packages/engine/src/duplicate-marker-clear.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Shared contracts for clearing an explicit DUPLICATE: marker without a real plan.
|
||||
*
|
||||
* FNXC:NearDuplicateDetection 2026-08-01-18:47:
|
||||
* Clearing a DUPLICATE marker must leave needs-replan + durable feedback +
|
||||
* nearDuplicateDismissed — never status:null. status:null is the planning-finished
|
||||
* signal the scheduler wakes on, so a prompt-less null-status card re-dispatches,
|
||||
* FS-fails, and storms (observed on FN-8704 / inactive FN-8676).
|
||||
*/
|
||||
|
||||
/** Log action picked up by triage's needs-replan feedback scanner. */
|
||||
export const TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION = "Duplicate marker cleared for re-specification";
|
||||
|
||||
export function buildInactiveDuplicateClearFeedback(canonicalId: string): string {
|
||||
return `Explicit duplicate marker targeting ${canonicalId} was cleared because that task is missing, deleted, done, or archived. Write a full PROMPT.md for this work. Do not re-emit DUPLICATE: ${canonicalId}.`;
|
||||
}
|
||||
|
||||
export function buildKeepDuplicateClearFeedback(canonicalId: string): string {
|
||||
return `Duplicate marker for ${canonicalId} was cleared (Keep / keep-acknowledged). Write a full PROMPT.md for this work. Do not re-emit DUPLICATE: ${canonicalId}.`;
|
||||
}
|
||||
|
||||
/** Patch applied when a marker is cleared so the card is unplanned, not "planning finished". */
|
||||
export function buildMarkerClearedReplanTaskPatch(canonicalId: string): {
|
||||
paused: false;
|
||||
pausedReason: null;
|
||||
status: "needs-replan";
|
||||
error: null;
|
||||
sourceMetadataPatch: {
|
||||
nearDuplicateOf: string;
|
||||
nearDuplicateScore: number;
|
||||
duplicateSource: "triage-marker";
|
||||
nearDuplicateDismissed: true;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: "needs-replan",
|
||||
error: null,
|
||||
sourceMetadataPatch: {
|
||||
nearDuplicateOf: canonicalId,
|
||||
nearDuplicateScore: 1,
|
||||
duplicateSource: "triage-marker",
|
||||
nearDuplicateDismissed: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import { runHoldReleaseSweep, isUnplannedForExecution, type SlotReservation } fr
|
||||
import { moveTaskToReplanColumn } from "./replan-target.js";
|
||||
import { evaluateParkedAgentTaskLink } from "./task-agent-sync.js";
|
||||
import { decideMissionSymbolAdmission, resolveMissionFeatureForTask } from "./mission-symbol-admission.js";
|
||||
import { computeRecoveryDecision, formatDelay, MAX_RECOVERY_RETRIES } from "./recovery-policy.js";
|
||||
|
||||
const SYMBOL_LOCK_LEASE_MS = 10 * 60_000;
|
||||
|
||||
@@ -2339,12 +2340,41 @@ export class Scheduler {
|
||||
const validation = await this.validateTaskFilesystem(task.id);
|
||||
if (!validation.valid) {
|
||||
schedulerLog.warn(`Task ${task.id} filesystem validation failed: ${validation.reason}`);
|
||||
// See the FNXC:WorkflowScheduling 2026-07-13-11:25 note in the legacy loop: the
|
||||
// status write is what makes triage rediscover a card whose replan column equals
|
||||
// its current column.
|
||||
/*
|
||||
FNXC:WorkflowScheduling 2026-08-01-18:47:
|
||||
Missing/empty PROMPT used to rebound with unbounded needs-replan writes (FN-8704
|
||||
storm twin). Share the planning recovery budget: backoff while attempts remain,
|
||||
then park failed so a broken task dir cannot spin forever. The status write is
|
||||
still what makes triage rediscover a card whose replan column equals its current column.
|
||||
*/
|
||||
const replanColumn = await moveTaskToReplanColumn(this.store, task);
|
||||
await this.store.updateTask(task.id, { status: "needs-replan" });
|
||||
await this.store.logEntry(task.id, `Task rebounded to ${replanColumn} for re-specification — filesystem validation failed`, validation.reason);
|
||||
const decision = computeRecoveryDecision({
|
||||
recoveryRetryCount: task.recoveryRetryCount,
|
||||
nextRecoveryAt: task.nextRecoveryAt,
|
||||
});
|
||||
if (!decision.shouldRetry) {
|
||||
const error = `REQUIRED_ARTIFACT_RECOVERY_EXHAUSTED: filesystem validation failed (${validation.reason}) after ${MAX_RECOVERY_RETRIES} automatic planning retries.`;
|
||||
await this.store.updateTask(task.id, {
|
||||
status: "failed",
|
||||
error,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
});
|
||||
await this.store.logEntry(task.id, error, validation.reason);
|
||||
return null;
|
||||
}
|
||||
const attempt = decision.nextState.recoveryRetryCount ?? MAX_RECOVERY_RETRIES;
|
||||
await this.store.updateTask(task.id, {
|
||||
status: "needs-replan",
|
||||
error: null,
|
||||
recoveryRetryCount: decision.nextState.recoveryRetryCount,
|
||||
nextRecoveryAt: decision.nextState.nextRecoveryAt,
|
||||
});
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`Task rebounded to ${replanColumn} for re-specification — filesystem validation failed (attempt ${attempt}/${MAX_RECOVERY_RETRIES} in ${formatDelay(decision.delayMs)})`,
|
||||
validation.reason,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ import { type TaskMoveLanes, resolveColumnFlags, IN_REVIEW_STALL_DEADLOCK_LOG_PR
|
||||
import { finalizePlanningSegment } from "@fusion/core";
|
||||
import type { MeshLeaseManager } from "./mesh-lease-manager.js";
|
||||
import { createLogger, schedulerLog } from "./logger.js";
|
||||
import {
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildInactiveDuplicateClearFeedback,
|
||||
buildKeepDuplicateClearFeedback,
|
||||
buildMarkerClearedReplanTaskPatch,
|
||||
} from "./duplicate-marker-clear.js";
|
||||
import { mergeEffectiveSettings } from "./effective-settings.js";
|
||||
import { RemovalReason, classifyTaskWorktree, getRegisteredWorktreeBranchMap, getRegisteredWorktreePaths, hasUsableWorktreeShape, isUsableTaskWorktree, relocateReclaimableWorktreeIntoRoot, removeWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "./worktree-pool.js";
|
||||
import {
|
||||
@@ -7064,12 +7070,31 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonical);
|
||||
if (!isNearDuplicateCanonicalInactive(canonical ?? undefined, canonicalFlags)) continue;
|
||||
|
||||
await this.store.updateTask(task.id, {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: null,
|
||||
sourceMetadataPatch: { nearDuplicateDismissed: true },
|
||||
});
|
||||
/*
|
||||
FNXC:NearDuplicateDetection 2026-08-01-18:47:
|
||||
Stale-decision recovery for an inactive canonical is the same writer as marker clear:
|
||||
needs-replan (not status:null) plus dismissal so the card cannot look planning-finished
|
||||
without a real PROMPT. Drop a still-present DUPLICATE marker file when present.
|
||||
*/
|
||||
const promptPath = join(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
if (existsSync(promptPath)) {
|
||||
try {
|
||||
const written = readFileSync(promptPath, "utf-8");
|
||||
if (parseExplicitDuplicateMarker(written)) {
|
||||
rmSync(promptPath, { force: true });
|
||||
}
|
||||
} catch {
|
||||
// best-effort marker removal; status write still proceeds
|
||||
}
|
||||
}
|
||||
await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalId));
|
||||
if (typeof this.store.logEntry === "function") {
|
||||
await Promise.resolve(this.store.logEntry(
|
||||
task.id,
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildInactiveDuplicateClearFeedback(canonicalId),
|
||||
)).catch(() => {});
|
||||
}
|
||||
await createRunAuditor(this.store, {
|
||||
runId: generateSyntheticRunId("reconcile-stale-duplicate-decision", task.id),
|
||||
agentId: "self-healing",
|
||||
@@ -14415,6 +14440,10 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
FN-8356 keeps maintenance from re-parking a marker against a missing, deleted, done,
|
||||
or archived canonical. Such a decision has no detail-banner action, so cleanup restores
|
||||
eligible work to planning while preserving explicit, implicit, and unrelated system pauses.
|
||||
|
||||
FNXC:NearDuplicateDetection 2026-08-01-18:47:
|
||||
Mirror triage: marker clear leaves needs-replan + feedback + dismissal, never
|
||||
status:null (FN-8704 replan storm when the scheduler wakes on planning→null without PROMPT).
|
||||
*/
|
||||
const canClearInactiveMarker = task.userPaused !== true
|
||||
&& (task.paused !== true || task.pausedReason === "duplicate-decision-required")
|
||||
@@ -14423,7 +14452,15 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
if (!canonicalTask || isNearDuplicateCanonicalInactive(canonicalTask, canonicalFlags)) {
|
||||
if (canClearInactiveMarker) {
|
||||
rmSync(promptPath, { force: true });
|
||||
await this.store.updateTask(task.id, { paused: false, pausedReason: null, status: null });
|
||||
const patch = buildMarkerClearedReplanTaskPatch(marker.canonicalId);
|
||||
await this.store.updateTask(task.id, patch);
|
||||
if (typeof this.store.logEntry === "function") {
|
||||
await Promise.resolve(this.store.logEntry(
|
||||
task.id,
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildInactiveDuplicateClearFeedback(marker.canonicalId),
|
||||
)).catch(() => {});
|
||||
}
|
||||
resolved += 1;
|
||||
}
|
||||
continue;
|
||||
@@ -14439,12 +14476,14 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
if (resolution === "prompt" && isTriageDuplicateKeepAcknowledged(task.sourceMetadata, canonicalTask.id)) {
|
||||
if (canClearInactiveMarker) {
|
||||
rmSync(promptPath, { force: true });
|
||||
await this.store.updateTask(task.id, {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: null,
|
||||
sourceMetadataPatch: { nearDuplicateDismissed: true },
|
||||
});
|
||||
await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalTask.id));
|
||||
if (typeof this.store.logEntry === "function") {
|
||||
await Promise.resolve(this.store.logEntry(
|
||||
task.id,
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildKeepDuplicateClearFeedback(canonicalTask.id),
|
||||
)).catch(() => {});
|
||||
}
|
||||
resolved += 1;
|
||||
}
|
||||
continue;
|
||||
@@ -14456,7 +14495,14 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
|
||||
await this.store.updateTask(task.id, { paused: true, pausedReason: "duplicate-decision-required", status: null });
|
||||
} else {
|
||||
rmSync(promptPath, { force: true });
|
||||
await this.store.updateTask(task.id, { paused: false, pausedReason: null, status: null, sourceMetadataPatch: { nearDuplicateOf: canonicalTask.id, nearDuplicateScore: 1, duplicateSource: "triage-marker", nearDuplicateDismissed: true } });
|
||||
await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalTask.id));
|
||||
if (typeof this.store.logEntry === "function") {
|
||||
await Promise.resolve(this.store.logEntry(
|
||||
task.id,
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildKeepDuplicateClearFeedback(canonicalTask.id),
|
||||
)).catch(() => {});
|
||||
}
|
||||
}
|
||||
log.log(`[self-healing] resolved explicit duplicate marker ${task.id} → ${canonicalTask.id}`);
|
||||
resolved += 1;
|
||||
|
||||
@@ -218,6 +218,12 @@ import {
|
||||
} from "./tool-availability.js";
|
||||
import { runGhostBugPreflight } from "./triage-preflight.js";
|
||||
import { archiveAsGhostBug } from "./self-healing.js";
|
||||
import {
|
||||
TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION,
|
||||
buildInactiveDuplicateClearFeedback,
|
||||
buildKeepDuplicateClearFeedback,
|
||||
buildMarkerClearedReplanTaskPatch,
|
||||
} from "./duplicate-marker-clear.js";
|
||||
import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js";
|
||||
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
|
||||
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
||||
@@ -2907,6 +2913,7 @@ export class TriageProcessor {
|
||||
|| entry.action === "User comment invalidated spec approval — task needs re-specification"
|
||||
|| entry.action === "AI spec revision requested"
|
||||
|| entry.action === TRIAGE_STUCK_RESUME_LOG_ACTION
|
||||
|| entry.action === TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION
|
||||
);
|
||||
feedback = feedbackLogEntry?.outcome;
|
||||
|
||||
@@ -4016,6 +4023,36 @@ export class TriageProcessor {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:NearDuplicateDetection 2026-08-01-18:47:
|
||||
Shared writer for every "delete the DUPLICATE marker and ask planning for a real plan"
|
||||
exit. Must leave status:needs-replan (not null), durable replan feedback, and
|
||||
nearDuplicateDismissed so (a) the scheduler's planning→null wake does not re-dispatch
|
||||
a prompt-less card, and (b) the next planner is told not to re-emit the same id.
|
||||
Outcome stays parked (default) — no Plan Review handoff until a real plan is written.
|
||||
*/
|
||||
private async clearDuplicateMarkerForReplan(
|
||||
task: Task,
|
||||
canonicalId: string,
|
||||
feedback: string,
|
||||
): Promise<boolean> {
|
||||
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
||||
await rm(join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
||||
})) return false;
|
||||
|
||||
try {
|
||||
await Promise.resolve(this.store.logEntry(task.id, TRIAGE_MARKER_CLEARED_REPLAN_LOG_ACTION, feedback));
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
planLog.warn(`${task.id}: failed to log marker-clear replan feedback: ${msg}`);
|
||||
}
|
||||
|
||||
return await this.updatePlanningStateIfStillCurrent(
|
||||
task,
|
||||
buildMarkerClearedReplanTaskPatch(canonicalId),
|
||||
);
|
||||
}
|
||||
|
||||
private async finalizeApprovedTaskBody(
|
||||
task: Task,
|
||||
writtenInput: string,
|
||||
@@ -4053,14 +4090,20 @@ export class TriageProcessor {
|
||||
view deliberately hides decisions for missing, deleted, done, or archived canonicals, so
|
||||
remove only the marker and return eligible work to planning instead of stranding its badge;
|
||||
explicit, implicit, and unrelated pauses are preserved.
|
||||
|
||||
FNXC:NearDuplicateDetection 2026-08-01-18:47:
|
||||
Clearing must leave needs-replan + feedback + dismissal — never status:null. A prompt-less
|
||||
null status is the scheduler's "planning finished" wake signal and re-opens the FN-8704
|
||||
replan storm (schedule → missing PROMPT → needs-replan → re-emit inactive DUPLICATE).
|
||||
*/
|
||||
const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
|
||||
if (isNearDuplicateCanonicalInactive(canonicalTask ?? undefined, canonicalFlags)) {
|
||||
if (canClearInactiveMarker) {
|
||||
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
||||
await rm(join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
||||
})) return;
|
||||
await this.updatePlanningStateIfStillCurrent(task, { paused: false, pausedReason: null, status: null });
|
||||
await this.clearDuplicateMarkerForReplan(
|
||||
task,
|
||||
canonicalId,
|
||||
buildInactiveDuplicateClearFeedback(canonicalId),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4075,15 +4118,11 @@ export class TriageProcessor {
|
||||
const keepAcknowledged = fusionCore.isTriageDuplicateKeepAcknowledged(task.sourceMetadata, canonicalId);
|
||||
if (resolution === "prompt" && keepAcknowledged) {
|
||||
if (canClearInactiveMarker) {
|
||||
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
||||
await rm(join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
||||
})) return;
|
||||
await this.updatePlanningStateIfStillCurrent(task, {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: null,
|
||||
sourceMetadataPatch: { nearDuplicateDismissed: true },
|
||||
});
|
||||
await this.clearDuplicateMarkerForReplan(
|
||||
task,
|
||||
canonicalId,
|
||||
buildKeepDuplicateClearFeedback(canonicalId),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4114,15 +4153,12 @@ export class TriageProcessor {
|
||||
await this.store.recordActivity({ type: "task:auto-archived-duplicate", taskId: task.id, details: "Flagged (not deleted) as triage-marker duplicate", metadata: { canonicalTaskId: canonicalId, source: "triage-marker-flagged" } });
|
||||
return;
|
||||
}
|
||||
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
||||
await rm(join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
||||
})) return;
|
||||
if (!await this.updatePlanningStateIfStillCurrent(task, {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
status: null,
|
||||
sourceMetadataPatch: { nearDuplicateOf: canonicalId, nearDuplicateScore: 1, duplicateSource: "triage-marker", nearDuplicateDismissed: true },
|
||||
})) return;
|
||||
// resolution === "keep" (and any other non-prompt/delete policy that drops the marker)
|
||||
await this.clearDuplicateMarkerForReplan(
|
||||
task,
|
||||
canonicalId,
|
||||
buildKeepDuplicateClearFeedback(canonicalId),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user