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:
1 parent
ba97f7cc2e
commit
86b55dbc91
4 files changed
+38
-7
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user