feat(claude-do): Worker: UsageGate — Queue ab Schwelle pausieren
> **Stand 2026-08-05 (List-Handler):** Der Roadblock aus dem letzten Lauf ist erledigt. Beide Voraussetzungen sind jetzt auf `main` gemerged: die `app_settings`-Schwellen `UsageGateFiveHourPct`/`UsageGateSevenDayPct` (Merge-Commit `b1efcdc`) und `UsageState`/`IUsageClient`/`UsageMonitorService` unter `src/ClaudeDo.Worker/Usage/` (Merge-Commit `b126a21`). Dein Worktree ist frisch von diesem `main` ClaudeDo-Task: 06a7cc32-6ab7-4758-98f4-bee77149b2bf
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class UsageGate : IUsageGate
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly UsageState _state;
|
||||
private readonly ILogger<UsageGate> _logger;
|
||||
|
||||
public UsageGate(IDbContextFactory<ClaudeDoDbContext> dbFactory, UsageState state, ILogger<UsageGate> logger)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<UsageGateDecision> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user