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

+68 -20
View File
@@ -100,6 +100,14 @@ async function showPayloadPreviewModal(sessionId) {
function _wfNum(n) { return Number(n || 0).toLocaleString('de-DE'); }
function _wfTime(t) { try { return new Date(t).toLocaleTimeString('de-DE'); } catch (e) { return ''; } }
function _wfDur(ms) {
ms = Number(ms || 0);
if (ms <= 0) return '–';
var s = Math.round(ms / 1000);
if (s < 60) return s + 's';
var m = Math.floor(s / 60);
return m + 'm ' + (s % 60) + 's';
}
function _renderWorkflowStatus(body, data) {
var status = (data && data.status) || 'none';
@@ -127,11 +135,22 @@ function _renderWorkflowStatus(body, data) {
return;
}
// Name / Zusammenfassung des Workflows (nur nach Abschluss vorhanden).
if (wf.workflowName || wf.summary) {
var title = document.createElement('div');
title.className = 'wf-title';
title.innerHTML = (wf.workflowName ? '<span class="wf-name">' + esc(wf.workflowName) + '</span>' : '') +
(wf.summary ? '<span class="wf-summary">' + esc(wf.summary) + '</span>' : '');
body.appendChild(title);
}
// Kennzahlen.
var stats = [
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
{ label: 'Tool-Aufrufe', value: _wfNum(wf.subagentToolCalls) },
{ label: 'Input-Tokens', value: _wfNum(wf.subagentInputTokens) },
{ label: 'Output-Tokens', value: _wfNum(wf.subagentOutputTokens) }
{ label: 'Fertig', value: _wfNum(wf.completedCount) + ' / ' + _wfNum(wf.subagentCount) },
{ label: 'Tokens', value: _wfNum(wf.totalTokens) },
{ label: 'Tool-Aufrufe', value: _wfNum(wf.totalToolCalls) },
{ label: status === 'running' ? 'Läuft seit' : 'Dauer', value: _wfDur(wf.durationMs) }
];
var grid = document.createElement('div');
grid.className = 'wf-stat-grid';
@@ -144,30 +163,59 @@ function _renderWorkflowStatus(body, data) {
});
body.appendChild(grid);
var tb = wf.toolBreakdown || {};
var keys = Object.keys(tb).sort(function (a, b) { return tb[b] - tb[a]; });
if (keys.length) {
var sec = document.createElement('div');
sec.className = 'wf-section';
sec.innerHTML = '<div class="wf-section-lbl">Tool-Aufschlüsselung</div>';
var list = document.createElement('div');
list.className = 'wf-tool-list';
keys.forEach(function (k) {
// Phasen (nur nach Abschluss).
var phases = wf.phases || [];
if (phases.length) {
var psec = document.createElement('div');
psec.className = 'wf-section';
psec.innerHTML = '<div class="wf-section-lbl">Phasen</div>';
var plist = document.createElement('div');
plist.className = 'wf-phase-list';
phases.forEach(function (p) {
var row = document.createElement('div');
row.className = 'wf-tool-row';
row.innerHTML = '<span class="wf-tool-name">' + esc(k) +
'</span><span class="wf-tool-count">' + esc(String(tb[k])) + '</span>';
list.appendChild(row);
row.className = 'wf-phase-row';
row.innerHTML = '<span class="wf-phase-title">' + esc(p.title || '') + '</span>' +
(p.detail ? '<span class="wf-phase-detail">' + esc(p.detail) + '</span>' : '');
plist.appendChild(row);
});
sec.appendChild(list);
body.appendChild(sec);
psec.appendChild(plist);
body.appendChild(psec);
}
// Subagenten-Details (nur nach Abschluss; live liegen noch keine Labels vor).
var agents = wf.agents || [];
if (agents.length) {
var asec = document.createElement('div');
asec.className = 'wf-section';
asec.innerHTML = '<div class="wf-section-lbl">Subagenten</div>';
var alist = document.createElement('div');
alist.className = 'wf-agent-list';
agents.forEach(function (a) {
var st = a.state || '';
var stCls = st === 'done' ? 'wf-ag-done' : (st === 'running' ? 'wf-ag-running' : 'wf-ag-other');
var row = document.createElement('div');
row.className = 'wf-agent-row';
row.innerHTML =
'<div class="wf-agent-main">' +
'<span class="wf-agent-state ' + stCls + '"></span>' +
'<span class="wf-agent-label">' + esc(a.label || ('Agent ' + (a.index || ''))) + '</span>' +
(a.phaseTitle ? '<span class="wf-agent-phase">' + esc(a.phaseTitle) + '</span>' : '') +
'</div>' +
'<div class="wf-agent-metrics">' +
'<span title="Tokens"><i class="fa fa-database"></i> ' + _wfNum(a.tokens) + '</span>' +
'<span title="Tool-Aufrufe"><i class="fa fa-wrench"></i> ' + _wfNum(a.toolCalls) + '</span>' +
'<span title="Dauer"><i class="fa fa-clock-o"></i> ' + _wfDur(a.durationMs) + '</span>' +
'</div>';
alist.appendChild(row);
});
asec.appendChild(alist);
body.appendChild(asec);
}
if (wf.startedAt) {
var ts = document.createElement('div');
ts.className = 'wf-meta';
ts.textContent = 'Start: ' + _wfTime(wf.startedAt) +
(wf.lastActivityAt ? ' · letzte Aktivität: ' + _wfTime(wf.lastActivityAt) : '');
ts.textContent = 'Start: ' + _wfTime(wf.startedAt);
body.appendChild(ts);
}
}