feat(worker): Handler-Session nach Usage-Limit-Reset automatisch neu starten

Eine List-Handler-ConPTY-Session, die mitten in einer Phase stirbt, war bisher
ein stilles totes Tile: InteractiveTerminalViewModel.OnSessionProcessExited
setzte nur bei einem Tod innerhalb des 5s-Grace-Fensters ein sichtbares Banner.

Neu:
- HandlerRoundState (JSON unter ~/.claudeDo/handler-rounds.json, keine
  EF-Migration): Phase + Task-Set der laufenden Runde, geschrieben von
  BuildForMergeHelperAsync ("triage") und BuildForMergeHelperHandoffAsync.
  Ohne das existieren beide nur im transienten HandoffRequested-Broadcast.
- HandlerRestartCoordinator: entscheidet den Neustart, fail-closed. Beleg fuer
  "war das Usage-Limit" ist FiveHour.Utilization >= 95 zum Todeszeitpunkt --
  eine ConPTY-Session hat kein stdout, TaskRunner.IsUsageLimitMessage hat hier
  nichts zu lesen. Nur bei PowerUse (beide Keys), max ein Neustart pro Runde,
  Warten bis FiveHour.ResetsAt, dann HandoffRequested erneut broadcasten.
  Verbraucht kein Handoff-Budget (HandoffRoundTracker.Register wird nicht
  angefasst). WorkerLog-Zeilen beim Tod und beim Neustart.
- Ui meldet den Exit-Code (nur dort bekannt) via
  WorkerHub.ReportInteractiveSessionExited, ohne selbst zu filtern.

Sibling zu UsageLimitAutoContinueCoordinator, kein Umbau davon: der sieht nur
headless Runs.
This commit is contained in:
mika kuns
2026-09-02 19:17:51 +02:00
parent 20b3b4ce2b
commit 016b9203f8
18 changed files with 843 additions and 12 deletions
+16 -1
View File
@@ -72,6 +72,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly TicketPatStore _ticketPat;
private readonly TicketClientFactory _ticketClients;
private readonly TicketImportService _ticketImport;
private readonly HandlerRestartCoordinator? _handlerRestart;
public WorkerHub(
QueueService queue,
@@ -110,7 +111,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
TicketSystemConfig ticketConfig = null!,
TicketPatStore ticketPat = null!,
TicketClientFactory ticketClients = null!,
TicketImportService ticketImport = null!)
TicketImportService ticketImport = null!,
HandlerRestartCoordinator? handlerRestart = null)
{
_queue = queue;
_waker = waker;
@@ -149,6 +151,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
_ticketPat = ticketPat;
_ticketClients = ticketClients;
_ticketImport = ticketImport;
_handlerRestart = handlerRestart;
}
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
@@ -786,6 +789,18 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
return _interactiveLaunchSpec.BuildForMergeHelperHandoffAsync(taskId, survivingTaskIds, nextPhase, Context.ConnectionAborted);
});
// Reports that an embedded ConPTY session's process exited. Only the UI process owns the
// terminal, so the exit code exists nowhere else; every decision about it (was this a list
// handler, did the usage limit kill it, is PowerUse on, when does the window reset) belongs to
// HandlerRestartCoordinator. Fire-and-forget on purpose: the coordinator's task stays parked
// until the 5h reset, which is far longer than this connection or this hub instance lives, so
// it must not run under Context.ConnectionAborted either.
public void ReportInteractiveSessionExited(string taskId, int exitCode)
{
if (_handlerRestart is null) return;
_ = _handlerRestart.OnSessionExitedAsync(taskId, exitCode, CancellationToken.None);
}
// Starts a planning session (worktree + prompt files + token, task -> Planning) and returns
// the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for
// StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started