fix(phase_runner): inject per-node claw agent aliases into topology graph
The topology graph shipped from team.graph only carries node.role, not node.agent. The executor then defaults to alias_for(role) which falls to ZEROCLAW_DEFAULT_AGENT (scout) — no such agent → 400. Look up team_members(node_id → claw_id) at enqueue time and stamp node.agent = claw_<hex> onto every node. Executor now dials the specific claw provisioned for THIS teams role. Was masked pre-C3 because the shared runtime hit the same 400 — never noticed because no one clicked through to a real run there.
This commit is contained in:
@@ -222,6 +222,11 @@ async fn launch_phase(
|
|||||||
for r in &team_rows {
|
for r in &team_rows {
|
||||||
let team_id: Uuid = r.get("team_id");
|
let team_id: Uuid = r.get("team_id");
|
||||||
let graph: serde_json::Value = r.get("graph");
|
let graph: serde_json::Value = r.get("graph");
|
||||||
|
// Inject each node's explicit `agent` alias so the executor
|
||||||
|
// dials the exact claw provisioned for THIS team's role,
|
||||||
|
// instead of falling through to the env-based ZEROCLAW_AGENT_MAP
|
||||||
|
// (which points at ambient names that don't exist per-team).
|
||||||
|
let graph = inject_node_agents(pool, team_id, graph).await;
|
||||||
let run_id = Uuid::now_v7();
|
let run_id = Uuid::now_v7();
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO topology_runs
|
"INSERT INTO topology_runs
|
||||||
@@ -347,3 +352,57 @@ async fn close_finished_missions(pool: &PgPool) -> Result<(), String> {
|
|||||||
.map_err(|e| format!("close finished missions: {e}"))?;
|
.map_err(|e| format!("close finished missions: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk the team's graph nodes and set `node.agent = "claw_<hex>"` for
|
||||||
|
/// each based on the `team_members(team_id, node_id, claw_id)` map.
|
||||||
|
/// Nodes without a matching member row are left alone (the executor
|
||||||
|
/// will fall through to the env alias map / default).
|
||||||
|
///
|
||||||
|
/// The graph shape we care about: `{ "nodes": [ { "id": "n0", ... } ] }`.
|
||||||
|
/// Non-object graphs (or graphs without a nodes array) are returned
|
||||||
|
/// unchanged.
|
||||||
|
async fn inject_node_agents(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
team_id: Uuid,
|
||||||
|
graph: serde_json::Value,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
let members = match sqlx::query(
|
||||||
|
"SELECT node_id, claw_id FROM team_members WHERE team_id = $1",
|
||||||
|
)
|
||||||
|
.bind(team_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(rows) => rows,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("phase_runner: load team_members({team_id}) failed: {e}");
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut by_node: std::collections::HashMap<String, Uuid> = std::collections::HashMap::new();
|
||||||
|
for row in members {
|
||||||
|
let node_id: String = row.get("node_id");
|
||||||
|
let claw_id: Uuid = row.get("claw_id");
|
||||||
|
by_node.insert(node_id, claw_id);
|
||||||
|
}
|
||||||
|
if by_node.is_empty() {
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
let mut graph = graph;
|
||||||
|
if let Some(nodes) = graph.get_mut("nodes").and_then(|v| v.as_array_mut()) {
|
||||||
|
for node in nodes {
|
||||||
|
let Some(obj) = node.as_object_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let id = obj.get("id").and_then(|v| v.as_str()).map(str::to_string);
|
||||||
|
let Some(id) = id else { continue };
|
||||||
|
if let Some(claw_id) = by_node.get(&id) {
|
||||||
|
obj.insert(
|
||||||
|
"agent".to_string(),
|
||||||
|
serde_json::Value::String(crate::runtime_provision::claw_alias(*claw_id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user