Reclaim safely abandoned workspace worktrees and canonical task branches without disrupting live or recoverable tasks. - Tear down eligible complete, idle failed, and soft-deleted per-repository worktrees with bounded retries. - Veto live, paused, scheduled-recovery, ambiguous, and unsafe-path cleanup candidates. - Verify failed-task landing evidence against each repository integration branch before deleting canonical branches. - Cover liveness and stale landed-SHA safety regressions. Files changed: .changeset/fn-9056-workspace-terminal-teardown.md | 7 + AGENTS.md | 2 +- docs/architecture.md | 1 + .../src/__tests__/self-healing-workspace.test.ts | 319 ++++++++++++++++++++- .../engine/src/executor/cleanup-task-worktree.ts | 8 +- packages/engine/src/self-healing.ts | 268 ++++++++++++----- packages/engine/src/util/run-audit.ts | 7 +- 7 files changed, 525 insertions(+), 87 deletions(-) Fusion-Task-Id: FN-9056 Fusion-Task-Lineage: 4353f7d9-063b-442b-88d3-6f3da1c9aae8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-08-03-15:40:
|
|
* TaskExecutor.cleanup peeled from TaskExecutor (U4).
|
|
*
|
|
* Drops in-memory active-worktree tracking and removes the single-repo worktree
|
|
* when no other task still needs it. Workspace roots are tracking-only (never removed).
|
|
*/
|
|
import type { TaskStore, WorkspaceConfig } from "@fusion/core";
|
|
import { findWorktreeUser } from "../merger.js";
|
|
import { RemovalReason } from "../worktree/worktree-pool.js";
|
|
import { executorLog } from "../logger.js";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror TaskExecutor method surface
|
|
type AnyFn = (...args: any[]) => any;
|
|
|
|
export type CleanupTaskWorktreeDeps = {
|
|
store: TaskStore;
|
|
workspaceConfig: WorkspaceConfig | null | undefined;
|
|
ensureWorkspaceConfig?: () => Promise<WorkspaceConfig | null>;
|
|
activeWorktrees: Map<string, Set<string>>;
|
|
getActiveWorktreePaths: (taskId: string) => string[];
|
|
removeOwnWorktreeWithReconcile: AnyFn;
|
|
};
|
|
|
|
export async function cleanupTaskWorktree(
|
|
deps: CleanupTaskWorktreeDeps,
|
|
taskId: string,
|
|
): Promise<void> {
|
|
const workspaceConfig = deps.ensureWorkspaceConfig
|
|
? await deps.ensureWorkspaceConfig()
|
|
: deps.workspaceConfig;
|
|
const worktreePaths = deps.getActiveWorktreePaths(taskId);
|
|
if (worktreePaths.length === 0) return;
|
|
|
|
deps.activeWorktrees.delete(taskId);
|
|
|
|
/*
|
|
FNXC:Workspace 2026-08-15-05:13:
|
|
In workspace mode the tracked path is the browse-only non-git root, never a removable worktree.
|
|
Per-repo teardown belongs to SelfHealingManager.reconcileOrphanedWorkspaceWorktrees for complete
|
|
and terminal lanes, while archive-lifecycle owns archived rows. A failed task may be retried, so
|
|
executor cleanup only drops in-memory tracking and must not discard sub-repo work at this boundary.
|
|
*/
|
|
if (workspaceConfig) {
|
|
return;
|
|
}
|
|
// Non-workspace tasks hold a one-element set — preserve the original single-path removal semantics.
|
|
const worktreePath = worktreePaths[0];
|
|
|
|
// Check if another task still needs this worktree
|
|
const otherUser = await findWorktreeUser(deps.store, worktreePath, taskId);
|
|
if (otherUser) {
|
|
executorLog.log(`Worktree retained for ${taskId} — still needed by ${otherUser}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const settings = await deps.store.getSettings();
|
|
await deps.removeOwnWorktreeWithReconcile({
|
|
worktreePath,
|
|
settings,
|
|
taskId,
|
|
reason: RemovalReason.ExecutorDispose,
|
|
});
|
|
executorLog.log(`Cleaned up worktree for ${taskId}`);
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
executorLog.error(`Failed to clean up worktree for ${taskId}:`, errorMessage);
|
|
}
|
|
}
|