using ClaudeDo.Data; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Usage.Interfaces; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Worker.Usage; public sealed record UsageGateDecision(bool IsBlocked, string? Reason); /// /// Gates autonomous queue refill on the 5h/7d Claude usage window. Fail-open: no snapshot yet, /// a failed last poll, or a settings read error all resolve to "not blocked" — an undocumented /// or unreachable usage API must never stall automation. Only queue refill is gated; runs already /// in flight (RunNow, ContinueTask, interactive sessions, planning, daily prep) are untouched. /// public sealed class UsageGate : IUsageGate { private readonly IDbContextFactory _dbFactory; private readonly UsageState _state; private readonly ILogger _logger; public UsageGate(IDbContextFactory dbFactory, UsageState state, ILogger logger) { _dbFactory = dbFactory; _state = state; _logger = logger; } public async Task EvaluateAsync(CancellationToken ct = default) { var snapshot = _state.Snapshot; if (snapshot is null || _state.LastError is not null) return new UsageGateDecision(false, null); int fiveHourPct; int sevenDayPct; try { using var context = _dbFactory.CreateDbContext(); var settings = await new AppSettingsRepository(context).GetAsync(ct); fiveHourPct = settings.UsageGateFiveHourPct; sevenDayPct = settings.UsageGateSevenDayPct; } catch (Exception ex) { _logger.LogWarning(ex, "UsageGate: failed to read gate thresholds; not blocking"); return new UsageGateDecision(false, null); } if (fiveHourPct > 0 && snapshot.FiveHour is not null && snapshot.FiveHour.Utilization >= fiveHourPct) return new UsageGateDecision(true, $"5h-Limit {snapshot.FiveHour.Utilization:0}% >= {fiveHourPct}%"); if (sevenDayPct > 0 && snapshot.SevenDay is not null && snapshot.SevenDay.Utilization >= sevenDayPct) return new UsageGateDecision(true, $"7d-Limit {snapshot.SevenDay.Utilization:0}% >= {sevenDayPct}%"); return new UsageGateDecision(false, null); } }