Read workflow-status telemetry from CLI transcript files

Dynamic ultracode workflows run as a detached background task with their
own transcript dir, so their subagent events never flow through the main
-p stream. The previous parent_tool_use_id tap therefore captured nothing
for real workflows and the status stayed "none" the whole run.

Replace the tap with a file-based reader:
- TapWorkflowLaunch parses the "Transcript dir:" line from the Workflow
  tool_result and registers the run.
- WorkflowProgressTracker becomes a per-session registry of transcript
  paths (no longer a stream accumulator).
- WorkflowTelemetryReader reads journal.jsonl for live counts (running)
  and <runId>.json for rich per-agent telemetry (completed); FinalizeChain
  persists the rich JSON into chains.workflow_info.
- Modal renders phases + a per-agent list (label/phase/state/tokens/
  tool-calls/duration) instead of the obsolete tool breakdown.

Sync ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md to the new
mechanism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-20 18:14:42 +02:00
1 parent b6e546f618
commit 2b95161596
9 files changed
+453 -175

No files matched your search

+16 -6
View File
@@ -162,7 +162,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id)
| **EmbeddingBackgroundService** | Backfill bestehender Messages + Live-Embedding neuer Messages/Memory-Files |
| **RemoteConnectionManager** | WebSocket-basierte Remote-Verbindungen, Command-Execution auf Remote-Clients |
| **StreamingTracker** | Tracking aktiver Streams + CancellationTokenSource pro Session für Stop |
| **WorkflowProgressTracker** | In-Memory-Telemetrie laufender dynamischer ultracode-Workflows pro Session (Subagent-Anzahl, Tool-Aufrufe + Aufschlüsselung, aggregierte Subagent-Tokens); LIVE während des Laufs, danach in `chains.workflow_info` persistiert (analog StreamingTracker) |
| **WorkflowProgressTracker** | **Registry** der Transcript-Pfade pro Session für dynamische ultracode-Workflows (`WorkflowRunRef`: ChainId/RunId/JournalPath/RichJsonPath/StartedAt) – kein Stream-Akkumulator. Die Telemetrie wird on demand über `WorkflowTelemetryReader` aus den CLI-Transcript-Dateien gelesen: `journal.jsonl` (live) bzw. `<runId>.json` (abgeschlossen, reich) und beim Finalize in `chains.workflow_info` persistiert |
| **AppPaths** | Zentrale Pfade (WwwRoot, Dev vs. Publish-Modus) (static) |
| **KeyScannerService** | API-Key-Scanner (GitHub Code Search, GitHub Issues/PRs, HuggingFace Hub, Google/Serper), täglicher Scan |
| **GitHubEventScannerService** | Echtzeit-Scanner: GitHub Public Events API (PushEvent-Diffs nach geleakten Keys), 30s Poll-Intervall |
@@ -358,7 +358,7 @@ Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten habe
**Member streamen LIVE** (wie der Session-Agent): CLI forwardet `TextDelta`/`ToolStart`/`ToolResult`, Anthropic-API streamt pro Tool-Runde über `NonStreamingLoopCallbacks`, OpenAI = one-shot (Text am Ende). Ein `SemaphoreSlim`-Write-Lock in `SseHelper.TryWriteSse` serialisiert die SSE-Byte-Flushes paralleler Member (sonst vermischen sich die Frames).
**Subagent-Filterung:** Spawnt ein CLI-Member intern einen Subagenten (Claude-CLI-`Agent`-Tool), sendet die CLI dessen Interna als separate Top-Level-Events mit `parent_tool_use_id`. Der Parser (`ClaudeCliService.RunCliStreamingAsync`) verwirft diese **zentral am Ursprung** (Guard auf nicht-leeres `parent_tool_use_id`) → keine namenlosen Tool-Boxen, weder im Live-Stream noch in der History (beide lesen aus denselben gefilterten `CliStreamEvent`s). Die `Agent`-Tool-Box selbst (`parent_tool_use_id=null`) bleibt sichtbar; der finale `result`-Event (mit den **aggregierten** Subagent-Tokens) wird nicht gefiltert → Token-Bilanz vollständig. Dieselben unterdrückten Subagent-Events werden zusätzlich **additiv** in den `WorkflowProgressTracker` abgegriffen (`TapSubagentEvent`, wirft nie) → speist den Workflow-Status, ohne den Guard/Stream zu verändern.
**Subagent-Filterung:** Spawnt ein CLI-Member intern einen Subagenten (Claude-CLI-`Agent`-Tool), sendet die CLI dessen Interna als separate Top-Level-Events mit `parent_tool_use_id`. Der Parser (`ClaudeCliService.RunCliStreamingAsync`) verwirft diese **zentral am Ursprung** (Guard auf nicht-leeres `parent_tool_use_id`) → keine namenlosen Tool-Boxen, weder im Live-Stream noch in der History (beide lesen aus denselben gefilterten `CliStreamEvent`s). Die `Agent`-Tool-Box selbst (`parent_tool_use_id=null`) bleibt sichtbar; der finale `result`-Event (mit den **aggregierten** Subagent-Tokens) wird nicht gefiltert → Token-Bilanz vollständig. Der Guard blendet diese klassischen Subagent-Events nur **aus** (`continue`) – er tappt sie nicht mehr ab. Der Workflow-Status der dynamischen ultracode-Läufe liest seine Telemetrie ohnehin aus den Transcript-Dateien (siehe unten), nicht aus dem Stream.
**Backend:** `ChatService.PrepareContext` baut `ChatContext.PanelAgents` (`List<SubAgentContext>`) + `MoaLayers`. `RunPanel(layers)` (AnthropicService.Panel.cs) läuft im ChatEndpoint-try VOR dem Worker-Dispatch: pro Runde Member parallel (`Task.WhenAll` + `SemaphoreSlim`), Provider-Weiche pro Member, **jede** Runde als `[Panel: Name]`-Message mit `Tag="panel"` persistiert (aus Worker-History gefiltert); JSON-Transkript = `{name, model, layer, totalLayers, segments:[Text/Tool…]}`. `NonStreamingLoopCallbacks.Persist=false` verhindert Doppel-Persistenz. Member-Tokens via `seedUsage` in Worker-`usage` gefaltet → Live-Badge **und** DB (kein separates Panel-Usage-Event). Synthese-Block in letzte User-Message gemergt. Panel-Fehler graceful.
@@ -375,13 +375,23 @@ Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten habe
## Workflow-Status (dynamische ultracode-Workflows)
Live-Telemetrie der Subagenten, die ein **ultracode**-Lauf über das `Workflow`/`Task`-Tool der Claude CLI spawnt (diese Subagent-Events sind im Haupt-Stream/Verlauf bewusst ausgeblendet, siehe Subagent-Filterung oben).
Telemetrie eines **ultracode**-Laufs, den die Claude CLI über das `Workflow`-Tool startet. **Entscheidend:** Solche Workflows laufen als **detachter Hintergrund-Task mit eigenem Transcript-Verzeichnis** – ihre Subagent-Events fließen NICHT durch den Haupt-(`-p`)-Stream (anders als klassische Task-Subagenten mit gesetztem `parent_tool_use_id`). Die echte Telemetrie liegt in zwei Dateien des CLI-Transcripts und wird **on demand** gelesen, nicht aus dem Stream abgegriffen.
**Backend:** `WorkflowProgressTracker` (Singleton, lokale Instanz in `Program.cs`, weil `ClaudeCliService` befüllt und `AnthropicService` beim Finalize persistiert – dieselbe Instanz). `ClaudeCliService.TapSubagentEvent` → `RecordSubagent(...)` führt pro Session In-Memory: Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung), aggregierte Input/Output-Tokens. Beim `FinalizeChain` (`AnthropicService.cs`) wird der Stand als JSON in `chains.workflow_info` (`ChainRecord.WorkflowInfo`, Migration via `TryAddColumn` in `DatabaseService.cs`, geschrieben in `DatabaseService.Sessions.cs` `UpdateChain`) geschrieben – mit ChainId-Abgleich, damit ein Panel-Member derselben Session ihn nicht löscht.
**Capture:** Im tool_result-Pfad ruft `ClaudeCliService.TapWorkflowLaunch(sessionId, chainId, resultContent)` auf; enthält der Inhalt „Workflow launched", parst die Methode die Zeile „Transcript dir: <absoluter Pfad>" und ruft `_workflowTracker.Register(...)`. Rein additiv, wirft nie. (Der `parent_tool_use_id`-Guard blendet klassische Subagent-Events weiterhin aus dem sichtbaren Stream aus – er tappt sie aber NICHT mehr.)
**Endpoint:** `GET /api/sessions/{id}/workflow-status` → LIVE-Snapshot aus dem Tracker, sonst persistiertes `workflow_info` der jüngsten Chain, sonst `status: "none"`. JSON: `{ sessionId, streaming, status (running/completed/none), chainId, workflow }`; das `workflow`-Objekt trägt camelCase `subagentCount`, `subagentToolCalls`, `subagentInputTokens`, `subagentOutputTokens`, `toolBreakdown`, `recentTools`, `startedAt`, `lastActivityAt`.
**Registry:** `WorkflowProgressTracker` (Singleton, lokale Instanz in `Program.cs`, weil `ClaudeCliService` befüllt und `AnthropicService` beim Finalize liest/persistiert – dieselbe Instanz) ist NUR eine Registry, kein Stream-Akkumulator. Speichert pro Session einen `WorkflowRunRef { ChainId, RunId, JournalPath, RichJsonPath, StartedAt }`, abgeleitet aus der Transcript-Dir. Methoden: `Register` / `Get` / `ChainIdFor` / `End` – in-memory pro Session, defensiv.
**Frontend:** Header-Menü-Eintrag **„Workflow-Status"** (`#workflowStatusBtn` in `index.html`, direkt nach „Payload anzeigen"), Wrapper `apiGetWorkflowStatus` (`api.js`), `showWorkflowStatusModal` (`modals.js`, pollt alle 2 s solange `status === 'running'`, stoppt von selbst beim Schließen des Modals), Click-Handler in `pages/chat-page.js`, `.workflow-status-modal`-Styles in `css/chat.css`.
**Reader:** Die statische `WorkflowTelemetryReader`-Klasse liest die Telemetrie aus zwei Quellen:
- LIVE `<transcriptDir>/journal.jsonl` (append-only; pro Subagent `started`/`result`-Zeilen) → `ReadLive` liefert `status: "running"`, `subagentCount` (= Anzahl `started`), `completedCount` (= Anzahl `result`), `durationMs`. Keine Tokens/Tool-Calls live verfügbar.
- ABGESCHLOSSEN `<proj>/workflows/<runId>.json` (erst am Laufende geschrieben) → `ReadCompleted` liefert `status: "completed"`, `workflowName`, `summary`, `durationMs`, `phases[]` (index/title/detail), `agents[]` (index/label/phaseTitle/state/model/tokens/toolCalls/durationMs/resultPreview), `totalTokens`, `totalToolCalls`, `subagentCount`, `completedCount`.
`Read()` bevorzugt die reiche, fertige `<runId>.json` und fällt sonst auf das Live-`journal.jsonl` zurück.
**Persistenz:** Beim `FinalizeChain` (`AnthropicService.cs`) wird die reiche `WorkflowProgress`-JSON in `chains.workflow_info` (`ChainRecord.WorkflowInfo`, Migration via `TryAddColumn` in `DatabaseService.cs`, geschrieben in `DatabaseService.Sessions.cs` `UpdateChain`) geschrieben – mit ChainId-Abgleich (`ChainIdFor`), damit ein Panel-Member derselben Session ihn nicht löscht; danach wird der Tracker-Eintrag via `End` freigegeben.
**Endpoint:** `GET /api/sessions/{id}/workflow-status` (`SessionEndpoint.HandleWorkflowStatus`), Reihenfolge: (1) ist für die Session ein Lauf registriert → Telemetrie aus den Transcript-Dateien (running aus `journal.jsonl` / completed aus `<runId>.json`); (2) sonst die jüngste aktive Chain mit nicht-leerem `workflow_info` → `status: "completed"`; (3) sonst `status: "none"`, `workflow: null`. JSON (running/completed): `{ sessionId, streaming, status, chainId, workflow }`; im `none`-Zweig: `{ sessionId, streaming, status: "none", workflow: null }` (KEIN `chainId`). Das `workflow`-Objekt trägt camelCase `status`, `chainId`, `workflowName`, `summary`, `startedAt`, `subagentCount`, `completedCount`, `totalTokens`, `totalToolCalls`, `durationMs`, `phases[]` (index/title/detail), `agents[]` (index/label/phaseTitle/state/model/tokens/toolCalls/durationMs/resultPreview).
**Frontend:** Header-Menü-Eintrag **„Workflow-Status"** (`#workflowStatusBtn` in `index.html`, direkt nach „Payload anzeigen"), Wrapper `apiGetWorkflowStatus` (`api.js`), `showWorkflowStatusModal` (`modals.js`, pollt alle 2 s solange `status === 'running'`, stoppt von selbst beim Schließen des Modals), Click-Handler in `pages/chat-page.js`, `.workflow-status-modal`-Styles in `css/chat.css`. Das Modal rendert **Phasen** + eine **Per-Agent-Liste** (Label, Phase, State, Tokens, Tool-Calls, Dauer) + Summen-Kacheln (`totalTokens`/`totalToolCalls`) – keine `toolBreakdown`/`recentTools` mehr.
---