"
).join("");
+// 2026-08-08 (Work Graph self-planning + loop-back): how many times
+// node_id was sent back to ITSELF (on_fail.action='retry_self' -- source
+// and target are the same node, so there's no on_failure edge to draw,
+// see work_graph_orchestrate.py's _LoopBackRequested/_on_failure_target
+// design comment). Returns 0 if run.loop_back_events is absent (older
+// runs/tasks predating this field) or empty.
+function countSelfLoopAttempts(run, nodeId) {
+ return (run.loop_back_events || []).filter(
+ (e) => e.source_node_id === nodeId && e.target_node_id === nodeId
+ ).length;
+}
+
function renderGraphVisualizerPanel(run, options) {
if (!run.graph_manifest || run.graph_manifest.nodes.length === 0) return "";
// FR-4 (v_graph_console, 2026-08-03): `options` is new and optional --
@@ -1149,10 +1161,19 @@ function renderGraphVisualizerPanel(run, options) {
const clickable = enableNodeClick && wn;
const clickableAttrs = clickable ? ' data-clickable-node-exec-id="' + wn.node_exec_id + '"' : "";
const logSlotHtml = clickable ? '' : "";
+ // 2026-08-08: retry_self loops never touch graph_manifest.edges (the
+ // target IS the source, so there's nothing to draw a line to) --
+ // the only place this is visible is a badge on the node itself,
+ // read from loop_back_events (execution_event-backed, see
+ // countSelfLoopAttempts's own comment).
+ const selfLoopCount = countSelfLoopAttempts(run, gn.node_id);
+ const loopBadgeHtml = selfLoopCount > 0
+ ? '↺ ' + selfLoopCount + ""
+ : "";
return (
- '
";
}).join("");
+ // 2026-08-08: on_failure edges (graph_manifest.edges, declared whenever a
+ // node has on_fail.action='loop_back') are the ones actually worth
+ // drawing as lines -- normal depends_on/needs_judgment/zero_token edges
+ // are already visually implied by row order (computeGraphLayers), so
+ // drawing them too would just be visual noise on top of what the rows
+ // already show. Each edge is annotated with how many times it was
+ // actually traversed (loop_back_events), not just declared -- a
+ // loop_back that was planned but never triggered still shows as a
+ // (0x) line, which is itself useful information.
+ const onFailureEdges = (run.graph_manifest.edges || []).filter((e) => e.edge_type === "on_failure");
+ const edgesDataJson = escapeHtml(JSON.stringify(onFailureEdges.map((e) => {
+ const triggerCount = (run.loop_back_events || []).filter(
+ (ev) => ev.source_node_id === e.from && ev.target_node_id === e.to
+ ).length;
+ return { from: e.from, to: e.to, triggerCount: triggerCount };
+ })));
+
return (
// id="graph-panel-": Org Graph 点击可达详情's scroll/highlight
// target (focusOrgNodePanel above) -- additive, doesn't change any
// existing class/data-* this function already emitted.
'
' + run.org_node + " Work Graph DAG 视图(v_graph_visualizer,run_id=" + run.run_id + ")