From 86b55dbc911e2d791bae946ea54e8e95a9b7af69 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 8 Nov 2026 11:32:16 +0000 Subject: [PATCH] fix(orchestrator): keep subagents behind their agent on the 3D time axis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- orchestrator/templates/page-graph3d.js | 10 ++++++---- orchestrator/templates/space-view.js | 23 ++++++++++++++++++++++- orchestrator/templates/space.js | 4 ++-- tests/test_hub_space.py | 8 ++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/orchestrator/templates/page-graph3d.js b/orchestrator/templates/page-graph3d.js index 01f4da0..0d3a3d3 100644 --- a/orchestrator/templates/page-graph3d.js +++ b/orchestrator/templates/page-graph3d.js @@ -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) { diff --git a/orchestrator/templates/space-view.js b/orchestrator/templates/space-view.js index 179da36..26bdfda 100644 --- a/orchestrator/templates/space-view.js +++ b/orchestrator/templates/space-view.js @@ -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) { diff --git a/orchestrator/templates/space.js b/orchestrator/templates/space.js index d76c6e7..7db337e 100644 --- a/orchestrator/templates/space.js +++ b/orchestrator/templates/space.js @@ -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, }; })(); diff --git a/tests/test_hub_space.py b/tests/test_hub_space.py index 3448ba7..3421a2f 100644 --- a/tests/test_hub_space.py +++ b/tests/test_hub_space.py @@ -92,6 +92,11 @@ console.log(JSON.stringify({ spawn: [0, 1, 2, 4, 9].map((level) => S.depthOf("spawn", {ts: now, level}, [low, now])), none: [S.depthOf("none", {ts: low, level: 3}, [low, now]), S.depthOf("none", {ts: now, level: 0}, [low, now])], }, + parents: S.notBeforeParent([ + {id: "c", kind: "chair", ts: 200}, {id: "s", kind: "session", parent: "c", ts: 100}, {id: "w", kind: "twig", parent: "v", ts: 120}, + {id: "t", kind: "twig", parent: "s", ts: 50}, {id: "u", kind: "twig", parent: "t"}, {id: "v", kind: "twig", parent: "s", ts: 150}, + {id: "x", kind: "subagent", parent: "gone", ts: 30}, {id: "y", kind: "twig", parent: "z", ts: 5}, {id: "z", kind: "twig", parent: "y", ts: 7}, + ], new Set(["twig", "subagent"])).map((node) => [node.id, node.ts]), same: [S.scene(input), S.scene(JSON.parse(JSON.stringify(input)))].every((other) => JSON.stringify(other) === JSON.stringify(built)), lines: Object.fromEntries(built.lines.map((entry) => [entry.sub || entry.node, {kind: entry.kind, level: entry.level, alive: entry.alive, stamps: entry.points.map((point) => point.ts), first: entry.points[0], name: entry.timeline[0].name}])), labels: built.labels.filter((label) => built.lines[label.line].node === "s5").map((label) => [label.at.ts, label.text]), @@ -174,6 +179,9 @@ class TestSpaceModule(unittest.TestCase): self.assertEqual(axis["spawn"], [-300, -150, 0, 300, 300]) self.assertEqual(axis["none"], [0, 0]) + def test_a_subagent_never_lies_before_the_agent_that_spawned_it(self): + self.assertEqual(self.data["parents"], [["c", 200], ["s", 100], ["w", 150], ["t", 100], ["u", 100], ["v", 150], ["x", 30], ["y", 7], ["z", 7]]) + def test_world_lines_are_deterministic_and_follow_the_layout_of_their_time(self): self.assertTrue(self.data["same"]) lines = self.data["lines"]