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:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
@@ -159,13 +160,13 @@ public sealed class TaskRunner
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, retryResult.ErrorMarkdown, retryResult.TurnCount,
|
||||
retryConfig.MaxTurns, ClassifyFailureReason(retryResult.TerminalReason));
|
||||
retryConfig.MaxTurns, ClassifyFailureReason(retryResult.TerminalReason, retryResult.ErrorMarkdown));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason, result.ErrorMarkdown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +277,7 @@ public sealed class TaskRunner
|
||||
else
|
||||
{
|
||||
await MarkFailed(taskId, task.Number, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason, result.ErrorMarkdown));
|
||||
}
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
@@ -603,13 +604,24 @@ public sealed class TaskRunner
|
||||
|
||||
internal static bool IsWriteTool(string toolName) => WriteTools.Contains(toolName);
|
||||
|
||||
/// <summary>Classifies the CLI's raw <c>terminal_reason</c> into the small, MCP-facing enum
|
||||
/// (<c>max_turns|timeout|error</c>) get_task/batch_get_tasks report as failureReason.
|
||||
// The CLI has no dedicated terminal_reason for a session/account usage-limit stop — it comes
|
||||
// back as a plain "api_error" with the provider's own text (session/rate limit + reset time)
|
||||
// riding in the result/error text (see ClaudeProcess.RunAsync). Matched against the rendered
|
||||
// failure message (BuildFailureMarkdown's output), never against Status alone.
|
||||
private static readonly Regex UsageLimitPattern = new(
|
||||
"usage limit|session limit|rate.?limit", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
internal static bool IsUsageLimitMessage(string? text) => !string.IsNullOrEmpty(text) && UsageLimitPattern.IsMatch(text);
|
||||
|
||||
/// <summary>Classifies the CLI's raw <c>terminal_reason</c> (plus, for "api_error", the
|
||||
/// rendered failure message) into the small, MCP-facing enum
|
||||
/// (<c>max_turns|timeout|usage_limit|error</c>) get_task/batch_get_tasks report as failureReason.
|
||||
/// "cancelled" is set explicitly at the call sites that know it (there's no CLI signal for it).</summary>
|
||||
internal static string ClassifyFailureReason(string? terminalReason) => terminalReason switch
|
||||
internal static string ClassifyFailureReason(string? terminalReason, string? messageText = null) => terminalReason switch
|
||||
{
|
||||
"max_turns" => "max_turns",
|
||||
"timeout" => "timeout",
|
||||
"api_error" when IsUsageLimitMessage(messageText) => "usage_limit",
|
||||
_ => "error",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user