fix(#2492): split rebuild-queue DAG lines on fan-out points, not just WCC
nodeComponents() split a DAG into weakly-connected components via deps edges, but post #2476/#2450 every agent's rebuild subgraph hangs off a shared MetaLock node via AfterOk, so the whole meta-update cascade is one connected component and rendered as a single wall-of-chips line. Add a second pass (splitFanOut) that further splits a component's topo-ordered nodes on out-degree>1 points: a node with more than one direct dependent renders as its own one-node line, and each dependent becomes the root of an independent line. Purely deps-structure-driven, same as the existing WCC split - no agent-field grouping involved. A component with no fan-out (the common single-agent case) is unaffected.
This commit is contained in:
parent
419c9659a3
commit
990f021c32
1 changed files with 62 additions and 2 deletions
|
|
@ -178,6 +178,64 @@ function entryAgents(entry) {
|
|||
return seen.join(',');
|
||||
}
|
||||
|
||||
// Further split one weakly-connected component's topo-ordered nodes on
|
||||
// *fan-out* points — a node with more than one direct dependent — so a
|
||||
// shared gate/lock node (e.g. MetaLock, which every agent's rebuild
|
||||
// subgraph now hangs off via AfterOk since the meta-update cascade was
|
||||
// folded into one in-DAG growth instead of separate per-agent child DAGs)
|
||||
// doesn't merge N otherwise-independent per-agent chains into one
|
||||
// wall-of-chips line. Pure
|
||||
// `deps`-structure-driven, same as the WCC split above — no `agent` field
|
||||
// involved. Rule: a node with out-degree > 1 renders as its own one-node
|
||||
// line; each of its direct dependents becomes the root of an independent
|
||||
// line, walked forward until the next fan-out point or a dead end. A
|
||||
// component with no fan-out (the common single-agent case) comes back
|
||||
// unchanged as one line.
|
||||
function splitFanOut(orderedNodes) {
|
||||
const ids = new Set(orderedNodes.map((n) => n.id));
|
||||
const children = new Map(orderedNodes.map((n) => [n.id, []])); // dep -> direct dependents
|
||||
for (const n of orderedNodes) {
|
||||
for (const d of n.deps || []) {
|
||||
if (children.has(d)) children.get(d).push(n.id);
|
||||
}
|
||||
}
|
||||
const byId = new Map(orderedNodes.map((n) => [n.id, n]));
|
||||
const indeg = new Map(orderedNodes.map((n) => [n.id, 0]));
|
||||
for (const n of orderedNodes) {
|
||||
for (const d of n.deps || []) {
|
||||
if (ids.has(d)) indeg.set(n.id, indeg.get(n.id) + 1);
|
||||
}
|
||||
}
|
||||
const roots = orderedNodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
|
||||
|
||||
const lines = [];
|
||||
const visited = new Set();
|
||||
function walk(startId, chain) {
|
||||
let cur = startId;
|
||||
while (cur != null && !visited.has(cur)) {
|
||||
visited.add(cur);
|
||||
const kids = children.get(cur) || [];
|
||||
if (kids.length > 1) {
|
||||
if (chain.length) lines.push(chain);
|
||||
lines.push([byId.get(cur)]);
|
||||
for (const k of kids) walk(k, []);
|
||||
return;
|
||||
}
|
||||
chain.push(byId.get(cur));
|
||||
cur = kids.length === 1 ? kids[0] : null;
|
||||
}
|
||||
if (chain.length) lines.push(chain);
|
||||
}
|
||||
for (const r of roots) walk(r, []);
|
||||
// Any leftover (shouldn't happen for a DAG reachable from its roots, but
|
||||
// guard against a dep loop / disconnected leftover rather than dropping
|
||||
// nodes from the display).
|
||||
for (const n of orderedNodes) {
|
||||
if (!visited.has(n.id)) lines.push([n]);
|
||||
}
|
||||
return lines.length ? lines : [orderedNodes];
|
||||
}
|
||||
|
||||
// Split a DAG's nodes into its actual weakly-connected subgraphs, using the
|
||||
// `deps` edges the backend provides — not an inferred heuristic like
|
||||
// grouping by `n.agent`. A DAG with independent subgraphs (e.g. a
|
||||
|
|
@ -185,7 +243,9 @@ function entryAgents(entry) {
|
|||
// component per subgraph; a single connected DAG stays one component. Each
|
||||
// component's nodes come back topo-sorted (Kahn's algorithm, falling back to
|
||||
// original array order for ties) so a chain renders in actual dependency
|
||||
// order rather than raw array order.
|
||||
// order rather than raw array order. Each component is then further split
|
||||
// on fan-out points (see `splitFanOut`) so a shared gate node doesn't merge
|
||||
// independent branches into one line.
|
||||
function nodeComponents(nodes) {
|
||||
if (!nodes.length) return [];
|
||||
const byId = new Map(nodes.map((n) => [n.id, n]));
|
||||
|
|
@ -243,7 +303,7 @@ function nodeComponents(nodes) {
|
|||
for (const cn of compNodes) {
|
||||
if (remaining.has(cn.id)) ordered.push(cn);
|
||||
}
|
||||
components.push(ordered);
|
||||
for (const line of splitFanOut(ordered)) components.push(line);
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue