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:
MelekiandClaude Opus 4.8 committed 2026-07-02 16:14:47 +02:00
1 parent 5f7dec6448
commit 3ae94fb697
9 files changed
+217 -19

No files matched your search

+42 -11
View File
@@ -90,9 +90,9 @@ public static class WorkflowTelemetryReader
{
if (run == null) return null;
var done = ReadCompleted(run.RichJsonPath);
if (done != null) { done.ChainId = run.ChainId; done.StartedAt = run.StartedAt; return done; }
if (done != null) { done.ChainId = run.ChainId; done.RunId = run.RunId; done.StartedAt = run.StartedAt; return done; }
var live = ReadLive(run.JournalPath, run.StartedAt);
if (live != null) { live.ChainId = run.ChainId; return live; }
if (live != null) { live.ChainId = run.ChainId; live.RunId = run.RunId; return live; }
return null;
}
@@ -158,14 +158,23 @@ public static class WorkflowTelemetryReader
catch { return null; }
}
/// <summary>Live-Fortschritt aus dem append-only journal.jsonl (started/result pro Subagent).</summary>
/// <summary>
/// Live-Fortschritt aus dem append-only journal.jsonl (started/result pro Subagent).
/// WICHTIG: Die <c>result</c>-Events enthalten den VOLLSTÄNDIGEN Subagent-Output (Feld
/// <c>result</c>, Objekt oder String) – die einzige Quelle, die schon WÄHREND des Laufs
/// existiert. Wir bauen daraus die Per-Agent-Liste inkl. Output-Preview, damit bei einem
/// Abbruch die bereits geleistete Arbeit fertiger Subagenten NICHT verloren geht (die reiche
/// &lt;runId&gt;.json mit Tokens/Labels wird erst bei Completion geschrieben). Tokens/Labels
/// sind hier daher noch 0/agentId – die kennt nur die abgeschlossene &lt;runId&gt;.json.
/// </summary>
public static WorkflowProgress ReadLive(string journalPath, DateTime startedAt)
{
if (string.IsNullOrEmpty(journalPath) || !File.Exists(journalPath)) return null;
try
{
var started = new HashSet<string>();
var done = new HashSet<string>();
var order = new List<string>(); // Reihenfolge der agentIds (started/result)
var seen = new HashSet<string>();
var outputs = new Dictionary<string, string>(); // agentId -> voller Output (nur fertige)
// ReadAllText statt ReadAllLines: robust gegen halb geschriebene letzte Zeile.
foreach (var raw in File.ReadAllText(journalPath).Split('\n'))
{
@@ -177,18 +186,39 @@ public static class WorkflowTelemetryReader
var t = GetStr(ev, "type");
var aid = GetStr(ev, "agentId");
if (string.IsNullOrEmpty(aid)) continue;
if (t == "started") started.Add(aid);
else if (t == "result") done.Add(aid);
if (seen.Add(aid)) order.Add(aid);
if (t == "result")
{
string output = null;
if (ev.TryGetProperty("result", out var rv))
output = rv.ValueKind == JsonValueKind.String ? rv.GetString() : rv.GetRawText();
outputs[aid] = output ?? "";
}
}
if (started.Count == 0 && done.Count == 0) return null;
return new WorkflowProgress
if (order.Count == 0) return null;
var wp = new WorkflowProgress
{
Status = "running",
StartedAt = startedAt,
SubagentCount = started.Count,
CompletedCount = done.Count,
SubagentCount = order.Count,
CompletedCount = outputs.Count,
DurationMs = (long)Math.Max(0, (DateTime.UtcNow - startedAt).TotalMilliseconds)
};
int idx = 0;
foreach (var aid in order)
{
var isDone = outputs.TryGetValue(aid, out var outp);
wp.Agents.Add(new WorkflowAgentInfo
{
Index = ++idx,
Label = aid,
State = isDone ? "done" : "running",
// Voller Output fertiger Subagenten (großzügig gekappt), damit die Arbeit
// bei Abbruch rekonstruierbar bleibt statt verloren zu gehen.
ResultPreview = isDone ? Trim(outp, 1500) : null
});
}
return wp;
}
catch { return null; }
}
@@ -215,6 +245,7 @@ public class WorkflowProgress
{
public string Status { get; set; } // "running" | "completed"
public string ChainId { get; set; }
public string RunId { get; set; } // wf_… – für Resume nach Abbruch
public string WorkflowName { get; set; }
public string Summary { get; set; }
public DateTime StartedAt { get; set; }