fix(orchestrator): keep subagents behind their agent on the 3D time axis

With depth = time, #agents/3d drew subagents behind the session that
spawned them. A subagent took the mtime of its meta.json, which is
rewritten when it finishes, and a session took the moment the hub first
saw its name, or the window end when it had none.

Sessions now take the first line of their transcript and subagents the
first line of theirs, both from /api/subagents, with the old values as
fallback. SpaceView.notBeforeParent moves a spawned node without a time,
or with one before its parent's, to its parent's time, nested subagents
included, so no subagent lies before the agent that spawned it. The 3D
signature carries the time, so the nodes move once the lifetimes load.

🎯 Fix: 93 %
💥 Break: 2 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 91
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-08 11:32:16 +00:00
1 parent ba97f7cc2e
commit 86b55dbc91
4 files changed
+38 -7

No files matched your search

+6 -4
View File
@@ -5,6 +5,7 @@
];
const KIND_COLORS = {operator: "accent", chair: "accent", session: "info", subagent: "violet", twig: "violet", repo: "warn", branch: "info", worktree: "teal", agent: "accent", artefact: "violet", task: "muted"};
const LINK_COLORS = {spawn: "violet", contains: "line-strong", merge: "grant", overlap: "alarm", direct: "direct", broadcast: "broadcast", alarm: "alarm", security: "security", grant: "grant"};
const SPAWNED = new Set(["twig", "subagent"]);
const DEPTH_GAP = 90;
const TIME_DEPTH = 640;
const g3 = {lib: null, three: null, graph: null, host: null, tab: "", sig: "", planes: [], colors: null, failed: ""};
@@ -61,13 +62,14 @@
function agentModel() {
const depthOf = {operator: 0, chair: 1, session: 2, subagent: 3};
const born = (node) => ((spawn.subs.get(node.id) || {}).data || {}).since;
const nodes = state.graph.nodes.map((node) => ({
id: node.id, kind: node.kind, label: node.label, depth: depthOf[node.kind], ts: (node.timeline[0] || {}).since || state.since,
id: node.id, kind: node.kind, label: node.label, depth: depthOf[node.kind], ts: born(node) || (node.timeline[0] || {}).since || state.since,
parent: node.kind === "operator" ? null : node.kind === "chair" ? HUMAN : node.kind === "session" ? chairKey() : node.parent || chairKey(),
}));
for (const node of state.graph.nodes.filter((entry) => entry.kind === "session" || entry.kind === "chair")) {
const walk = (twig, parent, depth) => {
nodes.push({id: twig.id, kind: "twig", label: twig.label, depth, ts: twig.row.started, parent, session: twig.session, sub: twig.row.id});
nodes.push({id: twig.id, kind: "twig", label: twig.label, depth, ts: twig.row.since || twig.row.started, parent, session: twig.session, sub: twig.row.id});
twig.children.forEach((child) => walk(child, twig.id, depth + 1));
};
twigs(node.id).forEach((twig) => walk(twig, node.id, depthOf[node.kind] + 1));
@@ -77,7 +79,7 @@
for (const link of aggregate().filter((entry) => entry.target !== "*" && ids.has(entry.source) && ids.has(entry.target) && entry.source !== entry.target)) {
links.push({source: link.source, target: link.target, kind: tone(link.cls), count: link.count});
}
return {nodes, links};
return {nodes: SpaceView.notBeforeParent(nodes, SPAWNED), links};
}
function model3d(tab) {
@@ -155,7 +157,7 @@
}
function signature3d(tab, data) {
return JSON.stringify([tab, layoutOf(), data.nodes.map((node) => [node.id, node.kind, node.depth]), data.links.map((link) => [link.source, link.target, link.kind])]);
return JSON.stringify([tab, layoutOf(), data.nodes.map((node) => [node.id, node.kind, node.depth, node.ts]), data.links.map((link) => [link.source, link.target, link.kind])]);
}
async function stage3d(tab, host) {
+22 -1
View File
@@ -74,6 +74,27 @@ const SpaceView = (() => {
return 0;
}
/**
* @param nodes - {id, kind, parent, ts} records.
* @param spawned - the kinds spawned by their parent, such as subagents.
* @returns copies of the nodes in which a spawned node without a time or
* with one before its parent's takes its parent's time.
*/
function notBeforeParent(nodes, spawned) {
const byId = new Map(nodes.map((node) => [node.id, node]));
const settled = new Map();
const at = (node) => {
if (!settled.has(node.id)) {
settled.set(node.id, node.ts);
const parent = spawned.has(node.kind) ? byId.get(node.parent) : undefined;
const floor = parent ? at(parent) : NaN;
settled.set(node.id, Number.isFinite(floor) && !(node.ts >= floor) ? floor : node.ts);
}
return settled.get(node.id);
};
return nodes.map((node) => ({...node, ts: at(node)}));
}
function clampView(kind, view) {
const limits = LIMITS[kind];
const wrapped = kind === "graph" ? {...view, yaw: signed(view.yaw)} : {...view, azimuth: around(view.azimuth)};
@@ -119,7 +140,7 @@ const SpaceView = (() => {
return {
DEFAULTS, LIMITS, AXES, DEPTH, LEVELS, REACH,
clamp, ease, turn, rotation, apply, camera, project, depthSort, depthOf, clampView, tidy, tween, lens, place, horizon, compass,
clamp, ease, turn, rotation, apply, camera, project, depthSort, depthOf, notBeforeParent, clampView, tidy, tween, lens, place, horizon, compass,
};
})();
if (typeof module === "object" && module.exports) {
+2 -2
View File
@@ -1,13 +1,13 @@
const Space = (() => {
"use strict";
const {DEFAULTS, LIMITS, AXES, DEPTH, REACH, ease, turn, rotation, apply, camera, project, depthSort, depthOf, clampView, tidy, tween, lens, place, horizon, compass} = typeof SpaceView === "object" ? SpaceView : require("./space-view.js");
const {DEFAULTS, LIMITS, AXES, DEPTH, REACH, ease, turn, rotation, apply, camera, project, depthSort, depthOf, notBeforeParent, clampView, tidy, tween, lens, place, horizon, compass} = typeof SpaceView === "object" ? SpaceView : require("./space-view.js");
const {TONES, ARC_CAP, bend, moon, nameAt, tree, capArcs, scene} = typeof SpaceScene === "object" ? SpaceScene : require("./space-scene.js");
const {paint} = typeof SpacePaint === "object" ? SpacePaint : require("./space-paint.js");
const {pick} = typeof SpacePick === "object" ? SpacePick : require("./space-pick.js");
return {
DEFAULTS, LIMITS, AXES, TONES, ARC_CAP, DEPTH, REACH,
ease, turn, bend, moon, rotation, apply, camera, project, depthSort, depthOf, clampView, tidy, tween, nameAt, tree, capArcs, scene, paint, pick,
ease, turn, bend, moon, rotation, apply, camera, project, depthSort, depthOf, notBeforeParent, clampView, tidy, tween, nameAt, tree, capArcs, scene, paint, pick,
lens, place, horizon, compass,
};
})();