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:
@@ -96,6 +96,13 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raised when the process died non-zero *after* clearing <see cref="StartupGraceWindow"/> —
|
||||
/// i.e. a session that ran, then broke. That used to be a silent dead tile with no signal at all;
|
||||
/// the worker turns it into a usage-limit auto-restart for list-handler sessions
|
||||
/// (<c>HandlerRestartCoordinator</c>). A startup death keeps using <see cref="StartError"/> instead:
|
||||
/// it never reached a state worth resuming.</summary>
|
||||
public event Action<int>? ExitedAfterStartup;
|
||||
|
||||
/// <summary>Internal (not private) so tests can drive it directly without a real ConPTY spawn.</summary>
|
||||
internal void OnSessionProcessExited(object? sender, int exitCode)
|
||||
{
|
||||
@@ -103,11 +110,16 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
||||
HasExited = true;
|
||||
ExitCode = exitCode;
|
||||
|
||||
if (exitCode == 0) return;
|
||||
if (_startedAtUtc is not { } startedAt) return; // never reached "running" — nothing ran to resume
|
||||
|
||||
// Died at startup: route through the same banner as a launch-time failure instead of
|
||||
// leaving a dead terminal with no Retry affordance (ConPtyPaneViewModel.CanRetry
|
||||
// requires StartError).
|
||||
if (exitCode != 0 && _startedAtUtc is { } startedAt && _utcNow() - startedAt < StartupGraceWindow)
|
||||
if (_utcNow() - startedAt < StartupGraceWindow)
|
||||
StartError = $"Session exited immediately (code {exitCode}).";
|
||||
else
|
||||
ExitedAfterStartup?.Invoke(exitCode);
|
||||
}
|
||||
|
||||
/// <summary>Reports a failure that happened before <see cref="Start"/> could be called (e.g. the
|
||||
|
||||
@@ -36,6 +36,12 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
|
||||
/// commits the worktree and moves the task to WaitingForReview.</summary>
|
||||
public event Action<string>? SubmitForReviewRequested;
|
||||
|
||||
/// <summary>Raised with <c>(taskId, exitCode)</c> when a task-based session that had been running
|
||||
/// dies non-zero. The host reports it to the worker, which decides whether this was a list
|
||||
/// handler killed by the usage limit and worth reopening. Ad-hoc panes never raise it — there is
|
||||
/// no task to reopen.</summary>
|
||||
public event Action<string, int>? SessionDiedAfterStartup;
|
||||
|
||||
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
||||
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.
|
||||
/// <paramref name="descriptorFactory"/> is resolved by the pane itself (in <see cref="Start"/>)
|
||||
@@ -49,7 +55,34 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
|
||||
TaskId = taskId;
|
||||
_displayTitle = displayTitle;
|
||||
_descriptorFactory = descriptorFactory;
|
||||
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||
Subscribe(Terminal);
|
||||
}
|
||||
|
||||
// Terminal is swapped wholesale on Retry, so the handlers follow the property rather than being
|
||||
// wired once at construction. (The initial value comes from the field initializer, which never
|
||||
// runs through the setter -- hence the explicit Subscribe in the constructor.)
|
||||
partial void OnTerminalChanging(InteractiveTerminalViewModel? oldValue, InteractiveTerminalViewModel newValue)
|
||||
{
|
||||
if (oldValue is not null) Unsubscribe(oldValue);
|
||||
}
|
||||
|
||||
partial void OnTerminalChanged(InteractiveTerminalViewModel value) => Subscribe(value);
|
||||
|
||||
private void Subscribe(InteractiveTerminalViewModel terminal)
|
||||
{
|
||||
terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||
terminal.ExitedAfterStartup += OnTerminalExitedAfterStartup;
|
||||
}
|
||||
|
||||
private void Unsubscribe(InteractiveTerminalViewModel terminal)
|
||||
{
|
||||
terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||
terminal.ExitedAfterStartup -= OnTerminalExitedAfterStartup;
|
||||
}
|
||||
|
||||
private void OnTerminalExitedAfterStartup(int exitCode)
|
||||
{
|
||||
if (TaskId is { } id) SessionDiedAfterStartup?.Invoke(id, exitCode);
|
||||
}
|
||||
|
||||
private readonly Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> _descriptorFactory;
|
||||
@@ -111,16 +144,15 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
|
||||
[RelayCommand(CanExecute = nameof(CanRetry))]
|
||||
private void Retry()
|
||||
{
|
||||
Terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||
Terminal.Dispose();
|
||||
Terminal = new InteractiveTerminalViewModel();
|
||||
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||
var dead = Terminal;
|
||||
Terminal = new InteractiveTerminalViewModel(); // OnTerminalChanging/Changed move the handlers
|
||||
dead.Dispose();
|
||||
Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||
Unsubscribe(Terminal);
|
||||
Terminal.Kill();
|
||||
Terminal.Dispose();
|
||||
}
|
||||
|
||||
@@ -413,10 +413,28 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||
pane.SessionDiedAfterStartup += OnPaneSessionDied;
|
||||
ConPtySessions.Add(pane);
|
||||
pane.Start();
|
||||
}
|
||||
|
||||
// A ConPTY session that had been running died. The UI process owns the terminal, so this is the
|
||||
// only place the exit code exists -- report it and let the worker decide whether it was a list
|
||||
// handler killed by the usage limit and worth reopening after the reset
|
||||
// (HandlerRestartCoordinator). Every other pane is just a no-op there, so no filtering here.
|
||||
private async void OnPaneSessionDied(string taskId, int exitCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _worker.ReportInteractiveSessionExitedAsync(taskId, exitCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A worker that is down cannot restart anything anyway; the dead tile is already visible.
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task<TerminalLaunchDescriptor> DescribeAsync(
|
||||
Func<System.Threading.Tasks.Task<LaunchSpec>> fetch)
|
||||
{
|
||||
@@ -457,6 +475,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
pane.ErrorReported -= OnConPtyPaneError;
|
||||
pane.CloseRequested -= CloseConPtySession;
|
||||
pane.SubmitForReviewRequested -= OnPaneSubmitForReview;
|
||||
pane.SessionDiedAfterStartup -= OnPaneSessionDied;
|
||||
ConPtySessions.Remove(pane);
|
||||
pane.Dispose();
|
||||
}
|
||||
@@ -507,6 +526,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
c.ErrorReported -= OnConPtyPaneError;
|
||||
c.CloseRequested -= CloseConPtySession;
|
||||
c.SubmitForReviewRequested -= OnPaneSubmitForReview;
|
||||
c.SessionDiedAfterStartup -= OnPaneSessionDied;
|
||||
c.Dispose();
|
||||
}
|
||||
ConPtySessions.Clear();
|
||||
|
||||
Reference in New Issue
Block a user