Workflow-Totalverlust beheben: inkrementelle Persistenz + Token-Fix + Resume
Bisher wurden Workflow-Subagent-Outputs nur beim sauberen FinalizeChain in die DB gerettet - bei Abbruch (529/Timeout/Stop/Hard-Kill) war alles weg, Tokens standen auf 0. - A) Inkrementelle Persistenz: MaybeFlushWorkflowProgress() (throttled 6s) schreibt workflow_info + Token + runId waehrend des Laufs -> ueberlebt Hard-Kill (OOM/Redeploy), wo kein catch/finally laeuft. - B) Output-Rettung: ReadLive liest jetzt vollstaendige Subagent-Outputs aus journal.jsonl (nicht nur Counts), inkl. State. - C) Token-Fix: neuer assistant-Branch -> CliEventType.Usage; EffectiveUsage() rettet Orchestrator-Token bei Abbruch statt 0. - D) Resume: Feld/Migration workflow_run_id; FinalizeChain persistiert die runId; naechster Chat injiziert einen resumeFromRunId-Hinweis. - UI: Workflow-Status-Modal zeigt geretteten Output + runId/"fortsetzbar"-Banner. Build 0/0, ReadLive gegen echtes journal.jsonl verifiziert, adversarial 2x geprueft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f7dec6448
commit
3ae94fb697
9 files changed
+217
-19
No files matched your search
@@ -54,6 +54,19 @@ public partial class AnthropicService
|
||||
// Panel-Member-Tokens (Fusion/MoA) vorab in die Chain-Usage falten → erscheinen im
|
||||
// done-Event (Live-Badge) UND in FinalizeChain (DB, überlebt F5). Null/leer = No-op.
|
||||
if (seedUsage != null) usage.Add(seedUsage);
|
||||
// Token-Fix bei Abbruch: Die Token stammen aus dem finalen result-Event – das kommt bei einem
|
||||
// Abbruch NIE, also bliebe usage 0. Wir akkumulieren die Orchestrator-Usage live aus den
|
||||
// assistant-Turns (CliEventType.Usage) in partialUsage und nehmen bei Abbruch/Zwischen-Persistenz
|
||||
// das jeweils Größere pro Feld (kein Doppelzählen mit dem normalen result-Pfad).
|
||||
var partialUsage = new TokenUsage();
|
||||
TokenUsage EffectiveUsage() => new TokenUsage
|
||||
{
|
||||
InputTokens = Math.Max(usage.InputTokens, partialUsage.InputTokens),
|
||||
OutputTokens = Math.Max(usage.OutputTokens, partialUsage.OutputTokens),
|
||||
CacheCreationTokens = Math.Max(usage.CacheCreationTokens, partialUsage.CacheCreationTokens),
|
||||
CacheReadTokens = Math.Max(usage.CacheReadTokens, partialUsage.CacheReadTokens)
|
||||
};
|
||||
var lastWfFlush = DateTime.MinValue; // Throttle für die inkrementelle Workflow-Persistenz
|
||||
var fullText = new StringBuilder();
|
||||
var tempImagePaths = new List<string>();
|
||||
string mcpConfigPath = null;
|
||||
@@ -119,6 +132,32 @@ public partial class AnthropicService
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resume-Hinweis: Lief in dieser Session zuletzt ein Workflow, der ABGEBROCHEN wurde,
|
||||
// injizieren wir seine runId in den System-Prompt. Der Agent kann den Workflow dann mit
|
||||
// resumeFromRunId erneut starten → bereits fertige Subagenten werden aus dem Journal
|
||||
// wiederverwendet statt neu ausgeführt. Best-effort (hängt an LLM-Kooperation). Nur bei
|
||||
// ultracode relevant (nur dann kann der Agent überhaupt das Workflow-Tool nutzen).
|
||||
if (ultracode && !string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var priorChains = await _db.GetActiveChains(sessionId); // ORDER BY id DESC
|
||||
// Nur der ZULETZT gelaufene Workflow zählt: ist er abgeschlossen → kein Resume;
|
||||
// wurde er abgebrochen → Hinweis anbieten.
|
||||
var lastWf = priorChains.FirstOrDefault(c =>
|
||||
c.ChainId != chainId && !string.IsNullOrEmpty(c.WorkflowRunId));
|
||||
if (lastWf != null && lastWf.StopReason != "end_turn"
|
||||
&& !(lastWf.WorkflowInfo != null && lastWf.WorkflowInfo.Contains("\"Status\":\"completed\"")))
|
||||
{
|
||||
systemText.AppendLine(
|
||||
$"\n<workflow_resume>\nIn dieser Session wurde zuletzt ein dynamischer Workflow (runId: {lastWf.WorkflowRunId}) vorzeitig beendet (Grund: {lastWf.StopReason ?? "unbekannt"}) und ist unvollständig. Falls die aktuelle Aufgabe die Fortsetzung genau dieses Workflows ist: starte ihn erneut mit DEMSELBEN Skript und übergib dem Workflow-Tool resumeFromRunId: \"{lastWf.WorkflowRunId}\" – bereits abgeschlossene Subagenten werden dann aus dem Journal wiederverwendet statt neu ausgeführt (spart Token und Zeit). Ist die aktuelle Aufgabe NICHT die Fortsetzung, ignoriere diesen Hinweis vollständig.\n</workflow_resume>");
|
||||
await SseHelper.TryWriteSse(response, "workflow_resume_available",
|
||||
new { runId = lastWf.WorkflowRunId, stopReason = lastWf.StopReason }, sseCt);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"[Wf-Resume/CLI] Error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
// Letzte User-Message als Prompt extrahieren
|
||||
var prompt = ExtractLastUserText(messages) ?? "";
|
||||
|
||||
@@ -282,7 +321,38 @@ public partial class AnthropicService
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CliEventType.Usage:
|
||||
// Orchestrator-Usage live akkumulieren (Token-Fix-Fallback für Abbruch):
|
||||
// Output/Cache summieren (pro Turn additiv), Input = größter Turn-Wert
|
||||
// (Kontextgröße wächst pro Turn – Summieren würde massiv doppelt zählen).
|
||||
partialUsage.OutputTokens += evt.OutputTokens;
|
||||
partialUsage.CacheReadTokens += evt.CacheReadTokens;
|
||||
partialUsage.CacheCreationTokens += evt.CacheCreationTokens;
|
||||
if (evt.InputTokens > partialUsage.InputTokens) partialUsage.InputTokens = evt.InputTokens;
|
||||
break;
|
||||
}
|
||||
// Inkrementelle Zwischen-Persistenz (throttled) – so überlebt der letzte Workflow-Stand
|
||||
// (Subagent-Outputs + Token) auch einen Hard-Kill (OOM/Redeploy), bei dem KEIN catch läuft.
|
||||
await MaybeFlushWorkflowProgress();
|
||||
}
|
||||
|
||||
// Persistiert – höchstens alle ~6 s – die aktuelle Workflow-Telemetrie dieser Chain in die DB.
|
||||
async Task MaybeFlushWorkflowProgress()
|
||||
{
|
||||
if (_workflowTracker == null || string.IsNullOrEmpty(sessionId)) return;
|
||||
if (_workflowTracker.ChainIdFor(sessionId) != chainId) return; // nur bei Workflow FÜR diese Chain
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - lastWfFlush).TotalSeconds < 6) return; // Throttle
|
||||
lastWfFlush = now;
|
||||
try
|
||||
{
|
||||
var run = _workflowTracker.Get(sessionId);
|
||||
var wp = WorkflowTelemetryReader.Read(run);
|
||||
if (wp == null) return;
|
||||
wp.ChainId = chainId;
|
||||
await _db.UpdateChainWorkflowProgress(chainId, JsonSerializer.Serialize(wp), run?.RunId, EffectiveUsage());
|
||||
}
|
||||
catch { /* inkrementelle Persistenz darf den Stream nie stören */ }
|
||||
}
|
||||
|
||||
do
|
||||
@@ -440,7 +510,7 @@ public partial class AnthropicService
|
||||
// Wenn ask_user_choice aktiv war: als awaiting_user_choice markieren,
|
||||
// damit die nächste Chain den Kontext (Frage + Optionen) behält.
|
||||
var stoppedReason = isUserChoice ? "awaiting_user_choice" : "stopped";
|
||||
await FinalizeChain(chain, usage, 1, stoppedReason, sw);
|
||||
await FinalizeChain(chain, EffectiveUsage(), 1, stoppedReason, sw);
|
||||
await SseHelper.TryWriteSse(response, "done", new
|
||||
{
|
||||
model = modelId,
|
||||
@@ -456,7 +526,7 @@ public partial class AnthropicService
|
||||
// Client-Disconnect (sseCt gecancelled, aber NICHT workCt)
|
||||
// CLI-Prozess läuft weiter im Hintergrund – hier nur Chain finalisieren
|
||||
// (sollte nicht mehr passieren, da CLI jetzt workCt nutzt)
|
||||
await FinalizeChain(chain, usage, 1, "client_disconnected", sw);
|
||||
await FinalizeChain(chain, EffectiveUsage(), 1, "client_disconnected", sw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -470,7 +540,7 @@ public partial class AnthropicService
|
||||
Content = errorMsg
|
||||
});
|
||||
await SseHelper.TryWriteSse(response, "error", new { error = errorMsg }, sseCt);
|
||||
await FinalizeChain(chain, usage, 1, "error", sw);
|
||||
await FinalizeChain(chain, EffectiveUsage(), 1, "error", sw);
|
||||
await SseHelper.TryWriteSse(response, "done", new
|
||||
{
|
||||
model = modelId,
|
||||
|
||||
Reference in new issue
Block a user