feat(worker): Handler-Session vor dem Usage-Limit warnen und Handoff bis zum Reset zurueckhalten
Eine PowerUse-Handler-Session soll am 5h-Limit nicht mehr mitten im Merge sterben, sondern vorher selbst sauber uebergeben: - HandlerUsageGuard (neu) warnt die laufende Session genau einmal pro Runde, sobald FiveHour.Utilization den Schwellwert PowerUseHandoffPct erreicht. Die Warnung wird per HandlerUsageWarning-Broadcast an Mission Control geschickt und dort in das Pane der Handler-TaskId getippt. - handoff_list_handler broadcastet den Handoff dann nicht sofort, sondern parkt ihn bis ResetsAt + 60 s; das Ergebnis traegt deferredUntil. Ohne ResetsAt wird wie bisher sofort gesendet. - Der geparkte Handoff liegt im HandlerRoundState (handler-rounds.json) und wird vom Guard-Tick ausgeloest, also auch nach einem Worker-Neustart wieder eingeplant bzw. sofort gesendet, wenn er ueberfaellig ist. - Review-Befund aus #272: HandlerRestartCoordinator wartet nicht mehr selbst bis ResetsAt, sondern parkt den Neustart im selben Slot. AutoRestartUsed wird erst gesetzt, wenn der Broadcast wirklich rausging - ein Worker-Absturz waehrend der Wartezeit laesst die Runde nicht mehr dauerhaft tot zurueck. Zurueckgehaltener Handoff und Auto-Neustart teilen sich einen Slot, also oeffnet nie beides dieselbe Runde. UsageLimitProofPct entfaellt zugunsten von PowerUseHandoffPct. - PtyTerminalSession.SendLineAsync schreibt in die laufende Session. Gegen eine echte claude-Session verifiziert: Text und Enter muessen zwei getrennte Writes sein, die Enter-Bytes kommen von Terminal.GenerateKeyInput.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Iciclecreek.Terminal;
|
||||
@@ -93,6 +94,56 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
ProcessExited?.Invoke(this, e.ExitCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types <paramref name="text"/> into the running session and submits it, exactly as if the user
|
||||
/// had typed it — used to warn a list-handler session before the usage limit
|
||||
/// (<c>HandlerUsageWarning</c>). Verified against a real `claude` TUI, including while it was
|
||||
/// mid-turn (the message is queued and acted on when the turn ends).
|
||||
///
|
||||
/// Three non-obvious details, all established by that spike:
|
||||
/// • <see cref="TerminalControl"/> exposes no public write path at all — its only public paste is
|
||||
/// <c>TerminalView.PasteAsync()</c>, which takes no argument and reads the clipboard. The
|
||||
/// private <c>SendToPtyAsync</c> on the inner view is the write seam, hence the reflection.
|
||||
/// • The text and the Enter must be TWO writes. A single <c>text + "\r"</c> chunk lands in
|
||||
/// claude's composer but is never submitted — a chunk carrying an embedded CR reads as pasted
|
||||
/// text, not as a keypress.
|
||||
/// • The Enter bytes come from the emulator (<c>GenerateKeyInput</c>) rather than a literal
|
||||
/// "\r", because claude turns Win32 input mode on and the emulator owns that encoding.
|
||||
///
|
||||
/// Best-effort by design: a package upgrade that renames the private member costs a warning that
|
||||
/// never arrives, never a crashed Mission Control.
|
||||
/// </summary>
|
||||
public async Task<bool> SendLineAsync(string text)
|
||||
{
|
||||
if (_control is not { } control || !IsRunning || string.IsNullOrEmpty(text)) return false;
|
||||
try
|
||||
{
|
||||
await SendRawAsync(control, text);
|
||||
await Task.Delay(SubmitDelay);
|
||||
await SendRawAsync(control, control.Terminal.GenerateKeyInput(XTerm.Input.Key.Enter, XTerm.Input.KeyModifiers.None));
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Long enough that the two writes can't be read as one chunk by the child's stdin reader.
|
||||
private static readonly TimeSpan SubmitDelay = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
private static readonly FieldInfo? s_terminalViewField =
|
||||
typeof(TerminalControl).GetField("_terminalView", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
private static readonly MethodInfo? s_sendToPty =
|
||||
typeof(TerminalView).GetMethod("SendToPtyAsync", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
private static Task SendRawAsync(TerminalControl control, string data)
|
||||
{
|
||||
if (s_terminalViewField?.GetValue(control) is not TerminalView view || s_sendToPty is null)
|
||||
throw new InvalidOperationException("Iciclecreek.Avalonia.Terminal no longer exposes a PTY write seam.");
|
||||
return (Task)s_sendToPty.Invoke(view, new object[] { data, CancellationToken.None })!;
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
try { _control?.Kill(); }
|
||||
|
||||
Reference in New Issue
Block a user