using ClaudeDo.Data; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Worker.Lifecycle; /// /// Startup-only sweep: dequeues queued tasks whose parent is missing or no longer /// in a planning phase. The child stays attached (ParentTaskId intact) but /// drops out of the queue so it can't run against a dead chain. The user can /// re-queue or detach manually. /// public sealed class OrphanRecovery : IHostedService { public const string Phase = "orphaned-children"; private readonly IDbContextFactory _dbFactory; private readonly HubBroadcaster _broadcaster; private readonly ILogger _logger; public OrphanRecovery( IDbContextFactory dbFactory, HubBroadcaster broadcaster, ILogger logger) { _dbFactory = dbFactory; _broadcaster = broadcaster; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken); var repo = new TaskRepository(ctx); var dequeued = await repo.DequeueOrphanedChildrenAsync(cancellationToken); if (dequeued > 0) _logger.LogWarning("Orphan recovery: dequeued {Count} stuck child task(s)", dequeued); else _logger.LogInformation("Orphan recovery: no stuck child tasks found"); await _broadcaster.OperationProgress(OperationProgressOpKeys.StartupRecovery, Phase, 0, 0); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }