feat(worker): cleanup planning worktree and branch on finalize/discard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-24 11:43:53 +02:00
parent fce91bcf86
commit 48899b3df8

View File

@@ -140,9 +140,21 @@ public sealed class PlanningSessionManager
public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
{
var (tasks, _, settings, ctx) = CreateRepos();
var (tasks, lists, settings, ctx) = CreateRepos();
await using var __ = ctx;
return await tasks.FinalizePlanningAsync(taskId, queueAgentTasks, ct);
var count = await tasks.FinalizePlanningAsync(taskId, queueAgentTasks, ct);
// Best-effort cleanup — don't block finalization on git state.
await TryCleanupWorktreeAsync(taskId, lists, settings, ct);
var sessionDir = Path.Combine(_rootDirectory, taskId);
if (Directory.Exists(sessionDir))
{
try { Directory.Delete(sessionDir, recursive: true); } catch { }
}
return count;
}
public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct)
@@ -155,16 +167,19 @@ public sealed class PlanningSessionManager
public async Task DiscardAsync(string taskId, CancellationToken ct)
{
var (tasks, _, settings, ctx) = CreateRepos();
var (tasks, lists, settings, ctx) = CreateRepos();
await using var __ = ctx;
var ok = await tasks.DiscardPlanningAsync(taskId, ct);
await TryCleanupWorktreeAsync(taskId, lists, settings, ct);
var sessionDir = Path.Combine(_rootDirectory, taskId);
if (Directory.Exists(sessionDir))
{
try { Directory.Delete(sessionDir, recursive: true); }
catch { /* best effort */ }
try { Directory.Delete(sessionDir, recursive: true); } catch { }
}
if (!ok)
throw new InvalidOperationException($"Task {taskId} was not in Planning state; nothing to discard.");
}
@@ -192,9 +207,49 @@ public sealed class PlanningSessionManager
var appSettings = await settings.GetAsync(ct);
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
var token = task.PlanningSessionToken ?? string.Empty;
if (!Directory.Exists(worktreePath))
throw new InvalidOperationException($"Planning worktree missing — cannot resume: {worktreePath}");
return new PlanningSessionResumeContext(taskId, listWorkingDir, task.PlanningSessionId, token, worktreePath);
var token = await ReadTokenFileAsync(TokenFilePathFor(sessionDir), ct);
return new PlanningSessionResumeContext(
ParentTaskId: taskId,
WorkingDir: worktreePath,
ClaudeSessionId: task.PlanningSessionId,
Token: token,
WorktreePath: worktreePath);
}
private async Task TryCleanupWorktreeAsync(
string taskId,
ListRepository lists,
AppSettingsRepository settings,
CancellationToken ct)
{
try
{
var (tasks, _, _, ctx2) = CreateRepos();
await using var __ = ctx2;
var task = await tasks.GetByIdAsync(taskId, ct);
if (task is null) return;
var list = await lists.GetByIdAsync(task.ListId, ct);
var listWorkingDir = list?.WorkingDir;
if (string.IsNullOrEmpty(listWorkingDir) || !Directory.Exists(listWorkingDir)) return;
var appSettings = await settings.GetAsync(ct);
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
var branchName = BranchNameFor(taskId);
if (Directory.Exists(worktreePath))
{
try { await _git.WorktreeRemoveAsync(listWorkingDir, worktreePath, force: true, ct); }
catch { /* best effort */ }
}
try { await _git.BranchDeleteAsync(listWorkingDir, branchName, force: true, ct); } catch { }
}
catch { /* best effort — never block finalize/discard */ }
}
private static string GenerateToken()