feat(worker): Toggle "Continue on session limit reset" für Usage-Limit-Abbrüche
Klassifiziert einen echten Usage-Limit-Abbruch als eigene FailureReason
"usage_limit" (TaskRunner.ClassifyFailureReason: nur bei terminal_reason
"api_error" plus einem Limit-Muster im gerenderten Fehlertext, nicht an
Status==Failed allein). Neuer Toggle AutoContinueOnUsageLimit (app_settings,
Default aus) unter Settings → General → "Usage limit stop":
- UsageLimitAutoContinueCoordinator feuert pro Task genau einmal ContinueTask
über OverrideSlotService, sobald das 5h-Fenster (UsageState.Snapshot.FiveHour
.ResetsAt) tatsächlich zurückgesetzt ist; ein persistenter Marker
(TaskEntity.UsageLimitAutoContinuedAt) verhindert einen zweiten Anlauf bei
einem erneuten Limit-Treffer.
- QueueService schedult zusätzlich einen exakten Wake-Timer auf den
Reset-Zeitpunkt, statt nur auf den 30s-Backstop zu warten.
- Fail-open durchgängig: kein Snapshot/keine Reset-Zeit → kein Timer, kein
Continue, kein Throw. Toggle aus ändert das heutige Verhalten nicht.
Migration AddUsageLimitAutoContinue fügt beide Spalten hinzu; die von
`dotnet ef migrations add` mitgescaffoldete leere UpdateData auf app_settings
(columns/values: []) erzeugte ungültiges SQL ("near WHERE") und wurde entfernt
— TaskNumberMigrationTests deckte das über den vollen Migrate()-Pfad auf.
This commit is contained in:
@@ -99,6 +99,29 @@ public class FailureDiagnosisTests
|
||||
{
|
||||
Assert.Equal(expected, TaskRunner.ClassifyFailureReason(terminalReason));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("You've hit your session limit – resets 1pm (Europe/Berlin)")]
|
||||
[InlineData("Claude usage limit reached, resets at 3pm")]
|
||||
[InlineData("rate limited, please retry later")]
|
||||
public void ClassifyFailureReason_ApiError_With_Limit_Text_Returns_UsageLimit(string messageText)
|
||||
{
|
||||
Assert.Equal("usage_limit", TaskRunner.ClassifyFailureReason("api_error", messageText));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyFailureReason_ApiError_With_Generic_Text_Stays_Error()
|
||||
{
|
||||
Assert.Equal("error", TaskRunner.ClassifyFailureReason("api_error", "Internal server error, please retry"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("max_turns")]
|
||||
[InlineData("timeout")]
|
||||
public void ClassifyFailureReason_Ignores_Limit_Text_For_NonApiError_Reasons(string terminalReason)
|
||||
{
|
||||
Assert.Equal(terminalReason, TaskRunner.ClassifyFailureReason(terminalReason, "usage limit reached"));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FailureDiagnosisEndToEndTests : IDisposable
|
||||
@@ -282,4 +305,32 @@ public sealed class FailureDiagnosisEndToEndTests : IDisposable
|
||||
Assert.Equal("error", task.FailureReason);
|
||||
Assert.NotEqual("max_turns", task.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Usage_Limit_Run_Sets_FailureReason_UsageLimit()
|
||||
{
|
||||
var dbFactory = _db.CreateFactory();
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T",
|
||||
Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
|
||||
{
|
||||
ExitCode = 1,
|
||||
TerminalReason = "api_error",
|
||||
ResultMarkdown = "You've hit your session limit – resets 1pm (Europe/Berlin)",
|
||||
}));
|
||||
var runner = MakeRunner(dbFactory, fake);
|
||||
|
||||
using (var ctx = _db.CreateContext())
|
||||
await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);
|
||||
|
||||
using var verify = _db.CreateContext();
|
||||
var task = await new TaskRepository(verify).GetByIdAsync("t1");
|
||||
Assert.Equal(TaskStatus.Failed, task!.Status);
|
||||
Assert.Equal("usage_limit", task.FailureReason);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user