Remote-Keep-Alive gegen 180s-Timeout + System-Prompt via Datei

Task 1 – Remote-Befehle laufen nicht mehr in einen starren 180s-Timeout:
- RemoteConnectionManager.ExecuteCommand nutzt jetzt einen INAKTIVITÄTS-
  Timeout (Watchdog auf LastProgressAt) statt hartem Wall-Clock-Limit; ein
  Befehl darf beliebig lange laufen, solange der Client Progress-Frames sendet.
- RemoteEndpoint: neuer "progress"-Case setzt die Frist zurück.
- InternalEndpoint (MCP-Pfad remote_execute): hartes CancellationTokenSource(180s)
  entfernt → nutzt konfigurierbares ExecuteTimeout-Setting + RequestAborted.
- remote.ps1 / remote.sh: senden während laufender Befehle alle 15s ein
  {"type":"progress"}-Keep-Alive; async stdout/stderr-Lesen gegen Pipe-Deadlocks.
- ToolService: Tool-Beschreibungen erklären den Inaktivitäts-Timeout.

Task 2 – System-Prompt über --system-prompt-file statt CLI-Argument:
- ClaudeCliService schreibt den vollen System-Prompt in eine Temp-Datei und
  übergibt --system-prompt-file, um das Windows-Kommandozeilen-Längenlimit zu
  umgehen. Cleanup im finally. Agent-Verhalten unverändert (MEMORY.md bleibt inline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-07-21 09:21:55 +02:00
1 parent 9db9bd4cc7
commit ace5036f00
7 files changed
+118 -26

No files matched your search

+47 -6
View File
@@ -18,6 +18,10 @@ public class RemoteConnection
public string OsVersion { get; set; }
public DateTime ConnectedAt { get; set; }
public DateTime LastActivityAt { get; set; }
// Zeitpunkt der letzten Aktivität WÄHREND eines laufenden Befehls (progress/result).
// Basis für den Inaktivitäts-Timeout in ExecuteCommand: Solange der Client periodisch
// Progress-Frames sendet, darf ein Befehl beliebig lange laufen.
public DateTime LastProgressAt { get; set; }
public string AssignedSessionId { get; set; }
public WebSocket Socket { get; set; }
@@ -92,8 +96,13 @@ public class RemoteConnectionManager
/// <summary>
/// Sendet einen Befehl an den Remote-Client und wartet auf das Ergebnis.
/// <paramref name="inactivityTimeoutSeconds"/> ist ein INAKTIVITÄTS-Timeout (kein hartes
/// Wall-Clock-Limit): Der Befehl darf beliebig lange laufen, solange der Client periodisch
/// Progress-Frames ({"type":"progress"}) sendet. Erst wenn für diese Dauer KEINE Aktivität
/// (progress oder result) mehr kommt, wird abgebrochen. So laufen schwere Aufgaben nicht mehr
/// in einen starren 180s-Timeout.
/// </summary>
public async Task<RemoteCommandResult> ExecuteCommand(string connectionId, string command, int timeoutSeconds = 180, CancellationToken ct = default)
public async Task<RemoteCommandResult> ExecuteCommand(string connectionId, string command, int inactivityTimeoutSeconds = 180, CancellationToken ct = default)
{
if (!_connections.TryGetValue(connectionId, out var conn))
return new RemoteCommandResult { ExitCode = -1, Error = "Verbindung nicht gefunden" };
@@ -105,26 +114,48 @@ public class RemoteConnectionManager
try
{
conn.PendingResult = new TaskCompletionSource<RemoteCommandResult>();
conn.LastProgressAt = DateTime.UtcNow;
// Befehl senden
var msg = JsonSerializer.Serialize(new { type = "execute", command });
var bytes = Encoding.UTF8.GetBytes(msg);
await conn.Socket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
// Auf Ergebnis warten (mit Timeout)
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
using var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled());
// Watchdog: bricht nur ab, wenn seit inactivityTimeoutSeconds keine Progress-/
// Result-Aktivität mehr vom Client kam. Progress-Frames setzen conn.LastProgressAt
// (siehe RemoteEndpoint.ReceiveLoop) und verlängern damit die Frist.
var watchdog = Task.Run(async () =>
{
try
{
while (!cts.IsCancellationRequested)
{
var idleSec = (DateTime.UtcNow - conn.LastProgressAt).TotalSeconds;
var remaining = inactivityTimeoutSeconds - idleSec;
if (remaining <= 0) { cts.Cancel(); return; }
// Häufig genug prüfen, aber nicht busy-spinnen (max 5s Takt)
await Task.Delay(TimeSpan.FromSeconds(Math.Min(remaining, 5)), cts.Token);
}
}
catch (OperationCanceledException) { }
}, cts.Token);
try
{
var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled());
var result = await conn.PendingResult.Task;
registration.Dispose();
return result;
}
catch (TaskCanceledException)
{
return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout nach {timeoutSeconds} Sekunden" };
return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout: keine Aktivität vom Remote-Client für {inactivityTimeoutSeconds}s" };
}
finally
{
cts.Cancel();
try { await watchdog; } catch { }
}
}
finally
@@ -134,6 +165,16 @@ public class RemoteConnectionManager
}
}
/// <summary>
/// Wird vom WebSocket-Receiver aufgerufen wenn ein Progress-/Keep-Alive-Frame vom Client kommt.
/// Setzt den Inaktivitäts-Zähler zurück, damit ein lang laufender Befehl nicht abgebrochen wird.
/// </summary>
public void HandleProgress(string connectionId)
{
if (_connections.TryGetValue(connectionId, out var conn))
conn.LastProgressAt = DateTime.UtcNow;
}
/// <summary>
/// Wird vom WebSocket-Receiver aufgerufen wenn ein Ergebnis vom Client kommt.
/// </summary>