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;
}