using ClaudeDo.Data; using ClaudeDo.Data.Repositories; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Worker.Lifecycle; /// /// Startup-only sweep that tries to re-attach blocked-by chains to their original /// planning parent when the lineage was lost (parent_task_id cleared, parent /// reverted to PlanningPhase.None) but the planning-sessions directory /// still has the matching folder. Only restores lineage when the chain in the /// parent's list is unambiguously the parent's — refuses to guess otherwise. /// public sealed class PlanningLineageRecovery : IHostedService { private readonly IDbContextFactory _dbFactory; private readonly string _sessionsRoot; private readonly ILogger _logger; public PlanningLineageRecovery( IDbContextFactory dbFactory, string sessionsRoot, ILogger logger) { _dbFactory = dbFactory; _sessionsRoot = sessionsRoot; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { if (!Directory.Exists(_sessionsRoot)) { _logger.LogInformation("Planning lineage recovery: sessions directory missing, nothing to scan"); return; } var folders = Directory.GetDirectories(_sessionsRoot); if (folders.Length == 0) { _logger.LogInformation("Planning lineage recovery: no session folders"); return; } await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken); var repo = new TaskRepository(ctx); var restored = 0; foreach (var folder in folders) { var taskId = Path.GetFileName(folder); if (string.IsNullOrEmpty(taskId)) continue; var count = await repo.RestorePlanningLineageAsync(taskId, cancellationToken); if (count > 0) { _logger.LogWarning( "Planning lineage recovery: re-attached {Count} child(ren) to parent {ParentId}", count, taskId); restored++; } } if (restored == 0) _logger.LogInformation("Planning lineage recovery: no candidates"); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }