fix: skip auto-archive when done task has active dependents

Auto-archive previously wiped .fusion/tasks/{id}/ for stale done tasks
even while downstream agents (triage/todo/in-progress/in-review) still
needed to read those sibling specs from disk. Now the sweep skips any
done task that has an active dependent. The executor prompt also
instructs the agent to fall back to fn_task_show when sibling spec
files are missing on disk (e.g., manually archived deps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 07:19:30 -07:00
parent ca30babd97
commit df20edb61f
4 changed files with 75 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Auto-archive sweep now skips done tasks that still have an active dependent (in triage, todo, in-progress, or in-review). Previously a stale done task could be archived while a downstream task was still pending, wiping its `.fusion/tasks/{id}/` directory and breaking the downstream agent's sibling-spec read. The agent prompt also now instructs falling back to `fn_task_show` when those sibling files aren't on disk.

View File

@@ -883,6 +883,54 @@ describe("SelfHealingManager", () => {
expect(store.archiveTaskAndCleanup).toHaveBeenCalledWith("FN-001");
expect(store.archiveTaskAndCleanup).not.toHaveBeenCalledWith("FN-002");
});
it("skips stale done tasks that have active dependents", async () => {
vi.setSystemTime(new Date("2026-01-04T00:00:00.000Z"));
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
autoArchiveDoneTasksEnabled: true,
autoArchiveDoneAfterMs: 24 * 60 * 60 * 1000,
} as unknown as Settings);
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
// Stale done — but a todo task depends on it. Must not be archived
// because archiving wipes .fusion/tasks/{id}/ and downstream agents
// are told they may read sibling task specs from disk.
{
id: "FN-100",
column: "done",
columnMovedAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
dependencies: [],
},
// Stale done — only a done dependent remains, archive is fine
{
id: "FN-101",
column: "done",
columnMovedAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
dependencies: [],
},
{
id: "FN-200",
column: "todo",
dependencies: ["FN-100"],
},
{
id: "FN-201",
column: "done",
// Fresh — not stale. Demonstrates that a *done* dependent does
// not block archive of FN-101.
columnMovedAt: "2026-01-03T23:00:00.000Z",
updatedAt: "2026-01-03T23:00:00.000Z",
dependencies: ["FN-101"],
},
]);
const result = await manager.archiveStaleDoneTasks();
expect(result).toBe(1);
expect(store.archiveTaskAndCleanup).toHaveBeenCalledWith("FN-101");
expect(store.archiveTaskAndCleanup).not.toHaveBeenCalledWith("FN-100");
});
});
// ── Completed task recovery ─────────────────────────────────────────

View File

@@ -401,7 +401,7 @@ You are running in an **isolated git worktree**. This means:
- **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment.
- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root to save durable project learnings (architecture patterns, conventions, pitfalls).
- **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task.
- **Exception — Sibling task specs:** You MAY read .fusion/tasks/{taskId}/PROMPT.md and .fusion/tasks/{taskId}/task.json at the project root (read-only) to consult dependency tasks' specifications.
- **Exception — Sibling task specs:** You MAY read .fusion/tasks/{taskId}/PROMPT.md and .fusion/tasks/{taskId}/task.json at the project root (read-only) to consult dependency tasks' specifications. If those files do not exist, the dependency has been archived — call \`fn_task_show\` with its ID to load the spec from the archive.
- **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree.
If you attempt to write to a path outside the worktree, the file tools will reject the operation with an error explaining the boundary.
@@ -7028,7 +7028,7 @@ You are running in an **isolated git worktree**. This means:
- **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree.
- **Exception — Project memory:** You MAY read and write to files under \`.fusion/memory/\` at the project root to save durable project learnings.
- **Exception — Task attachments:** You MAY read files under \`.fusion/tasks/{taskId}/attachments/\` at the project root for context.
- **Exception — Sibling task specs:** You MAY read \`.fusion/tasks/{taskId}/PROMPT.md\` and \`.fusion/tasks/{taskId}/task.json\` at the project root (read-only) to consult dependency tasks' specifications.
- **Exception — Sibling task specs:** You MAY read \`.fusion/tasks/{taskId}/PROMPT.md\` and \`.fusion/tasks/{taskId}/task.json\` at the project root (read-only) to consult dependency tasks' specifications. If those files do not exist, the dependency has been archived — call \`fn_task_show\` with its ID to load the spec from the archive.
- **Shell commands** run inside the worktree by default. Avoid using \`cd\` to navigate outside the worktree.
## Begin

View File

@@ -712,6 +712,20 @@ export class SelfHealingManager {
const tasks = await this.store.listTasks({ slim: true, includeArchived: false });
const cutoff = Date.now() - archiveAfterMs;
// Build a set of task IDs that have at least one *active* dependent —
// i.e., another task in triage/todo/in-progress/in-review that lists
// this ID in its `dependencies`. Archiving such a task wipes
// `.fusion/tasks/{id}/` on disk, which downstream agents are told they
// may read for sibling-spec context (executor prompt). Done/archived
// dependents have already consumed the spec and don't block.
const tasksWithActiveDependents = new Set<string>();
for (const t of tasks) {
if (t.column === "done" || t.column === "archived") continue;
for (const depId of t.dependencies ?? []) {
tasksWithActiveDependents.add(depId);
}
}
const stale = tasks.filter((t) => {
if (t.column !== "done") return false;
// Prefer columnMovedAt (when the task entered done); fall back to updatedAt
@@ -719,7 +733,12 @@ export class SelfHealingManager {
const ts = t.columnMovedAt || t.updatedAt;
const movedAt = ts ? Date.parse(ts) : NaN;
if (!Number.isFinite(movedAt)) return false;
return movedAt < cutoff;
if (movedAt >= cutoff) return false;
if (tasksWithActiveDependents.has(t.id)) {
log.log(`Skipping auto-archive of ${t.id}: has active dependents`);
return false;
}
return true;
});
if (stale.length === 0) return 0;