Security: the pawns already orbited their destination, so homing them at
the security station gave circling for free. This adds the radial
press-and-retreat — an agent closing on the target and backing off reads
as probing it, where a fixed radius reads as waiting — and holds the
stochastic target release while probing, or the circling breaks up into
stray trips that look like distraction rather than a scan.
Findings are `mission_tasks` rows, one orb each, popped once. There is
deliberately no severity anywhere in the path: the scanner keeps
severity, file and line as substrings inside `title`, so a severity
parsed out of prose and rendered as an orb's RADIUS would be the picture
asserting a measurement the data never contained. Count only.
Benchmarks annotate the station, as text. `delta` has no schema —
compute_delta emits `{kind:"opaque"}` whenever the before/after metrics
were not structurally comparable, which is most drivers. The server
formats the shape it can parse and COUNTS the rest; an unparseable driver
reports "3 sample(s)" rather than an invented improvement, and an opaque
delta says nothing at all.
The finished map: the live label rule gates service/event nodes on
`heat > 0.12`, which is exactly backwards once everything has cooled — a
static map would be unlabelled dots. Frozen, the 25 most-touched nodes
label regardless of heat, phase stations carry a second line counting
what they produced, and the camera is released ONCE so it frames the
result even if the user panned during the run.
Every count in a caption is read off the drawn scene rather than a
parallel tally, so the words and the picture cannot disagree.
Co-Authored-By: Claude Opus 5 <[email protected]>
1362 lines
57 KiB
Rust
1362 lines
57 KiB
Rust
//! `GET /api/world/live` — the Clawmates Event Taxonomy as a Server-Sent Events
|
||
//! feed for the World + Observe visualizations. Read-only and workspace-scoped.
|
||
//!
|
||
//! Phase 1 emits the *real* shape of the workspace: a `topology.update` of its
|
||
//! agents, an `agent.status` per agent (working when it holds a live container,
|
||
//! else idle), and a `telemetry` snapshot — polled, mirroring `run_events_sse`.
|
||
//!
|
||
//! The richer `world.touch` / `agent.tool.call` events (an agent converging on
|
||
//! the node it acts upon — the Gource centerpiece) come from normalizing the
|
||
//! durable runner's `run_events`; that is the extension seam (see `normalize`),
|
||
//! filled in as the runner emits node targets. Until then the client's synthetic
|
||
//! fallback supplies that motion.
|
||
|
||
use std::collections::HashSet;
|
||
use std::convert::Infallible;
|
||
use std::time::Duration;
|
||
|
||
use axum::extract::{Query, State};
|
||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||
use axum::response::IntoResponse;
|
||
use axum::Json;
|
||
use cm_domain::WorkspaceId;
|
||
use serde::Deserialize;
|
||
use serde_json::{json, Value};
|
||
use sqlx::{PgPool, Row};
|
||
use uuid::Uuid;
|
||
|
||
use crate::{ApiError, AppState, Authed};
|
||
|
||
fn sse(event: &str, data: Value) -> Result<Event, Infallible> {
|
||
Ok(Event::default().event(event).data(data.to_string()))
|
||
}
|
||
|
||
/// Agents that currently hold a live container (any kind) → "working".
|
||
async fn working_agents(pool: &PgPool, ws: WorkspaceId) -> HashSet<String> {
|
||
let rows = sqlx::query(
|
||
"SELECT DISTINCT a.id::text AS id
|
||
FROM agents a JOIN agent_containers ac ON ac.agent_id = a.id
|
||
WHERE a.workspace_id = $1",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter().map(|r| r.get::<String, _>("id")).collect()
|
||
}
|
||
|
||
/// One row per (mission, agent) pair for every mission the World should draw.
|
||
///
|
||
/// Three bugs were fixed here at once, and each hid the next:
|
||
///
|
||
/// 1. **The join was on the wrong column.** It read
|
||
/// `JOIN team_members tm ON tm.team_id = m.team_id`, but `missions.team_id`
|
||
/// is a legacy pointer at the FIRST minted team — `0056_mission_teams.sql`
|
||
/// superseded it with the `mission_teams(mission_id, team_id, purpose)`
|
||
/// junction, which is what everything else (including `/api/workforce`)
|
||
/// joins through. A multi-team mission showed only its first team.
|
||
///
|
||
/// 2. **It was an INNER JOIN, and microVM missions have no team at all.**
|
||
/// `mission_orchestrator::on_launch` deliberately mints none for them, so
|
||
/// the platform's primary execution tier was dropped by the join and the
|
||
/// World has been showing nothing whatsoever for it. LEFT JOIN, and
|
||
/// `agent_id` is `None` for those — a phase that ran with no platform
|
||
/// agents draws no pawns, which is the truth rather than a gap.
|
||
///
|
||
/// 3. **Only `running` missions were selected.** Missions finish in minutes,
|
||
/// so the World was empty almost always. Recently-finished ones come back
|
||
/// too, carrying `status` so the client can draw a finished map instead of
|
||
/// animating a corpse.
|
||
struct MissionRow {
|
||
mission_id: String,
|
||
title: String,
|
||
status: String,
|
||
template_kind: String,
|
||
completed_at: Option<String>,
|
||
/// `None` for a mission with no team — see (2) above.
|
||
agent_id: Option<String>,
|
||
}
|
||
|
||
async fn world_missions(
|
||
pool: &PgPool,
|
||
ws: WorkspaceId,
|
||
only: Option<Uuid>,
|
||
) -> Vec<MissionRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT m.id::text AS mission_id,
|
||
m.title AS title,
|
||
m.status AS status,
|
||
m.template_kind AS template_kind,
|
||
to_char(m.completed_at AT TIME ZONE 'UTC',
|
||
'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS completed_at,
|
||
tm.claw_id::text AS agent_id
|
||
FROM missions m
|
||
LEFT JOIN mission_teams mt ON mt.mission_id = m.id
|
||
LEFT JOIN team_members tm ON tm.team_id = mt.team_id
|
||
WHERE m.workspace_id = $1
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($2::uuid IS NULL OR m.id = $2)
|
||
ORDER BY m.created_at DESC",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.map(|r| MissionRow {
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
title: r.get::<String, _>("title"),
|
||
status: r.get::<String, _>("status"),
|
||
template_kind: r.get::<String, _>("template_kind"),
|
||
completed_at: r.get::<Option<String>, _>("completed_at"),
|
||
agent_id: r.get::<Option<String>, _>("agent_id"),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Every phase of the given missions, in plan order, with the agents that
|
||
/// execute it.
|
||
///
|
||
/// The whole plan is emitted — including `pending` phases that have not
|
||
/// started — because the World draws the mission's shape upfront and lights it
|
||
/// as it progresses. A phase list that only appeared as phases began would make
|
||
/// a five-phase mission indistinguishable from a one-phase mission until it was
|
||
/// nearly over.
|
||
struct PhaseRow {
|
||
mission_id: String,
|
||
phase_id: String,
|
||
kind: String,
|
||
order_idx: i32,
|
||
status: String,
|
||
iteration: i32,
|
||
started_at: Option<String>,
|
||
completed_at: Option<String>,
|
||
}
|
||
|
||
async fn world_phases(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<PhaseRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT mp.mission_id::text AS mission_id,
|
||
mp.id::text AS phase_id,
|
||
mp.kind AS kind,
|
||
mp.order_idx AS order_idx,
|
||
mp.status AS status,
|
||
COALESCE(mp.iteration, 0) AS iteration,
|
||
to_char(mp.started_at AT TIME ZONE 'UTC',
|
||
'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS started_at,
|
||
to_char(mp.completed_at AT TIME ZONE 'UTC',
|
||
'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS completed_at
|
||
FROM mission_phases mp
|
||
JOIN missions m ON m.id = mp.mission_id
|
||
WHERE m.workspace_id = $1
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($2::uuid IS NULL OR m.id = $2)
|
||
ORDER BY mp.mission_id, mp.order_idx",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.map(|r| PhaseRow {
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
phase_id: r.get::<String, _>("phase_id"),
|
||
kind: r.get::<String, _>("kind"),
|
||
order_idx: r.get::<i32, _>("order_idx"),
|
||
status: r.get::<String, _>("status"),
|
||
iteration: r.get::<i32, _>("iteration"),
|
||
started_at: r.get::<Option<String>, _>("started_at"),
|
||
completed_at: r.get::<Option<String>, _>("completed_at"),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Files a phase touched, from the `code_diff` artifact captured at delivery.
|
||
///
|
||
/// This is recorded fact, not inference: `mission_delivery` writes the path
|
||
/// list with the same revision and excludes it uses for `files_changed`, so the
|
||
/// orbs the World draws are the files git says changed.
|
||
///
|
||
/// It is end-of-phase detail — the capture runs when a phase finishes — so a
|
||
/// running phase shows its station lit but no files until it lands. Live
|
||
/// per-tool file touches are a separate, structured source.
|
||
struct FileRow {
|
||
mission_id: String,
|
||
phase_id: String,
|
||
path: String,
|
||
status: String,
|
||
}
|
||
|
||
async fn world_files(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<FileRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT ma.mission_id::text AS mission_id,
|
||
ma.phase_id::text AS phase_id,
|
||
f->>'path' AS path,
|
||
f->>'status' AS status
|
||
FROM mission_artifacts ma
|
||
JOIN missions m ON m.id = ma.mission_id
|
||
CROSS JOIN LATERAL jsonb_array_elements(
|
||
COALESCE(ma.metadata->'files', '[]'::jsonb)) AS f
|
||
WHERE m.workspace_id = $1
|
||
AND ma.kind = 'code_diff'
|
||
AND ma.phase_id IS NOT NULL
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($2::uuid IS NULL OR m.id = $2)
|
||
LIMIT 2000",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.filter_map(|r| {
|
||
Some(FileRow {
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
phase_id: r.get::<String, _>("phase_id"),
|
||
path: r.get::<Option<String>, _>("path")?,
|
||
status: r.get::<Option<String>, _>("status").unwrap_or_default(),
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One row of `mission_events` — what an agent actually did.
|
||
struct ActRow {
|
||
id: i64,
|
||
mission_id: String,
|
||
phase_id: Option<String>,
|
||
agent_id: Option<String>,
|
||
kind: String,
|
||
target: String,
|
||
}
|
||
|
||
/// Structured mission activity since `after`, oldest first.
|
||
///
|
||
/// `after < 0` means "the first pass has not run yet": everything is returned
|
||
/// so the caller can seed its cursor and draw the backlog as settled history.
|
||
async fn world_acts(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>, after: i64) -> Vec<ActRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT e.id,
|
||
e.mission_id::text AS mission_id,
|
||
e.phase_id::text AS phase_id,
|
||
e.agent_id::text AS agent_id,
|
||
e.kind,
|
||
e.target
|
||
FROM mission_events e
|
||
JOIN missions m ON m.id = e.mission_id
|
||
WHERE m.workspace_id = $1
|
||
AND e.kind IN ('tool.call', 'file.touch')
|
||
AND e.target IS NOT NULL
|
||
AND e.id > $2
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($3::uuid IS NULL OR m.id = $3)
|
||
ORDER BY e.id
|
||
LIMIT 500",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(after)
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.filter_map(|r| {
|
||
Some(ActRow {
|
||
id: r.get::<i64, _>("id"),
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
phase_id: r.get::<Option<String>, _>("phase_id"),
|
||
agent_id: r.get::<Option<String>, _>("agent_id"),
|
||
kind: r.get::<String, _>("kind"),
|
||
target: r.get::<Option<String>, _>("target")?,
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One security finding, as the scanner recorded it.
|
||
struct FindingRow {
|
||
mission_id: String,
|
||
phase_id: String,
|
||
task_id: String,
|
||
title: String,
|
||
}
|
||
|
||
/// Findings raised by a security phase.
|
||
///
|
||
/// They are `mission_tasks` rows — the scanner's own store — not a new table.
|
||
/// There is deliberately NO severity field here: severity, file and line are
|
||
/// substrings inside `title` (see `security_scan.rs`), and a severity parsed
|
||
/// out of prose and then encoded as an orb's RADIUS would be an invented fact
|
||
/// rendered as a measurement. The title is shown as written.
|
||
async fn world_findings(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<FindingRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT t.mission_id::text AS mission_id,
|
||
t.phase_id::text AS phase_id,
|
||
t.id::text AS task_id,
|
||
t.title
|
||
FROM mission_tasks t
|
||
JOIN mission_phases p ON p.id = t.phase_id
|
||
JOIN missions m ON m.id = t.mission_id
|
||
WHERE m.workspace_id = $1
|
||
AND p.kind = 'security_scan'
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($2::uuid IS NULL OR m.id = $2)
|
||
LIMIT 300",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.map(|r| FindingRow {
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
phase_id: r.get::<String, _>("phase_id"),
|
||
task_id: r.get::<String, _>("task_id"),
|
||
title: r.get::<String, _>("title"),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// A benchmark phase's result, summarised.
|
||
struct BenchRow {
|
||
mission_id: String,
|
||
phase_id: String,
|
||
note: String,
|
||
}
|
||
|
||
/// Benchmark deltas, as an annotation on the phase — not as nodes.
|
||
///
|
||
/// `delta` is a `Record<string, unknown>` with no schema: `compute_delta`
|
||
/// produces `{kind:"bencher_diff", samples:[…]}` when the before/after shapes
|
||
/// are structurally comparable, and `{kind:"opaque"}` when they are not. Only
|
||
/// the shape that can be parsed is formatted; the rest is COUNTED, never
|
||
/// guessed at, so a driver whose output we do not understand reports "3
|
||
/// samples" rather than an invented improvement.
|
||
async fn world_benchmarks(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<BenchRow> {
|
||
let rows = sqlx::query(
|
||
"SELECT DISTINCT ON (b.phase_id)
|
||
b.mission_id::text AS mission_id,
|
||
b.phase_id::text AS phase_id,
|
||
b.delta
|
||
FROM benchmark_snapshots b
|
||
JOIN missions m ON m.id = b.mission_id
|
||
WHERE m.workspace_id = $1
|
||
AND b.delta IS NOT NULL
|
||
AND ( m.status = 'running'
|
||
OR ( m.status IN ('completed', 'failed')
|
||
AND m.completed_at > now() - interval '24 hours' ) )
|
||
AND ($2::uuid IS NULL OR m.id = $2)
|
||
ORDER BY b.phase_id, b.iteration DESC",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(only)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.filter_map(|r| {
|
||
let note = benchmark_note(&r.get::<serde_json::Value, _>("delta"))?;
|
||
Some(BenchRow {
|
||
mission_id: r.get::<String, _>("mission_id"),
|
||
phase_id: r.get::<String, _>("phase_id"),
|
||
note,
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One line describing a benchmark delta, or `None` if there is nothing
|
||
/// truthful to say about it.
|
||
fn benchmark_note(delta: &serde_json::Value) -> Option<String> {
|
||
let samples = delta.get("samples").and_then(|s| s.as_array())?;
|
||
if samples.is_empty() {
|
||
return None;
|
||
}
|
||
let mut improved = 0usize;
|
||
let mut regressed = 0usize;
|
||
// Best (most negative) percent change, since that is the one claim the
|
||
// shape actually supports.
|
||
let mut best: Option<f64> = None;
|
||
for s in samples {
|
||
match s.get("direction").and_then(|d| d.as_str()) {
|
||
Some("improved") => improved += 1,
|
||
Some("regressed") => regressed += 1,
|
||
// Counted in the total but claimed for neither side.
|
||
_ => {}
|
||
}
|
||
if let Some(pct) = s.get("delta_pct").and_then(serde_json::Value::as_f64) {
|
||
best = Some(best.map_or(pct, |b: f64| b.min(pct)));
|
||
}
|
||
}
|
||
let mut parts = vec![format!("{} sample(s)", samples.len())];
|
||
if improved > 0 {
|
||
parts.push(format!("{improved} faster"));
|
||
}
|
||
if regressed > 0 {
|
||
parts.push(format!("{regressed} slower"));
|
||
}
|
||
if let Some(b) = best.filter(|b| *b < 0.0) {
|
||
parts.push(format!("best {:.1}%", b));
|
||
}
|
||
Some(parts.join(", "))
|
||
}
|
||
|
||
/// Agents that execute a phase of this kind, via the purposes the phase runner
|
||
/// itself uses. Returns empty for a teamless (microVM) mission.
|
||
async fn phase_agents(
|
||
pool: &PgPool,
|
||
mission_id: &str,
|
||
kind: &str,
|
||
) -> Vec<String> {
|
||
let Ok(mid) = Uuid::parse_str(mission_id) else {
|
||
return Vec::new();
|
||
};
|
||
// `purposes_for` is the runner's own mapping, shared rather than copied —
|
||
// see its doc comment.
|
||
let purposes: Vec<String> = crate::phase_runner::purposes_for(kind)
|
||
.iter()
|
||
.map(|s| s.to_string())
|
||
.collect();
|
||
let rows = sqlx::query(
|
||
"SELECT DISTINCT tm.claw_id::text AS agent_id
|
||
FROM mission_teams mt
|
||
JOIN team_members tm ON tm.team_id = mt.team_id
|
||
WHERE mt.mission_id = $1 AND mt.purpose = ANY($2)",
|
||
)
|
||
.bind(mid)
|
||
.bind(&purposes)
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.map(|r| r.get::<String, _>("agent_id"))
|
||
.collect()
|
||
}
|
||
|
||
/// Currently-running runs in the workspace as (run_id, agent_id) — each is a
|
||
/// real "this agent is converging on its active work" signal (Gource).
|
||
async fn active_runs(pool: &PgPool, ws: WorkspaceId) -> Vec<(String, String)> {
|
||
let rows = sqlx::query(
|
||
"SELECT ar.id::text AS run_id, s.agent_id::text AS agent_id
|
||
FROM agent_runs ar JOIN sessions s ON s.id = ar.session_id
|
||
WHERE s.workspace_id = $1 AND ar.state = 'running'",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
rows.into_iter()
|
||
.map(|r| (r.get::<String, _>("run_id"), r.get::<String, _>("agent_id")))
|
||
.collect()
|
||
}
|
||
|
||
/// A short human label for a tool's input (for the tool-call target).
|
||
fn summarize_input(input: &Value) -> String {
|
||
for k in ["target", "path", "url", "query", "name", "file", "command"] {
|
||
if let Some(s) = input.get(k).and_then(|v| v.as_str()) {
|
||
return s.chars().take(48).collect();
|
||
}
|
||
}
|
||
String::new()
|
||
}
|
||
|
||
/// Normalize one durable `run_events` row into taxonomy events — the Rust twin of
|
||
/// the handoff bridge's normalize(). The runner already journals these, so the
|
||
/// live view shows REAL reasoning, tool-convergence and doors with no runner edit.
|
||
fn normalize_run_event(
|
||
agent_id: &str,
|
||
event_type: &str,
|
||
payload: &Value,
|
||
) -> Vec<(&'static str, Value)> {
|
||
let mut out = Vec::new();
|
||
match event_type {
|
||
"text_delta" => {
|
||
if let Some(delta) = payload.get("delta").and_then(|v| v.as_str()) {
|
||
out.push((
|
||
"agent.reasoning.delta",
|
||
json!({ "agentId": agent_id, "text": delta }),
|
||
));
|
||
}
|
||
}
|
||
"step_started" => {
|
||
let tool = payload
|
||
.get("tool")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("tool");
|
||
let node_id = format!("tool:{tool}");
|
||
let target = payload
|
||
.get("input")
|
||
.map(summarize_input)
|
||
.unwrap_or_default();
|
||
// File/project I/O gets an explosive burst (high touch weight).
|
||
let lower = tool.to_lowercase();
|
||
let file_op = [
|
||
"file", "read", "write", "drive", "vault", "obsidian", "edit", "fs", "save",
|
||
]
|
||
.iter()
|
||
.any(|k| lower.contains(k));
|
||
let weight = if file_op { 1.0 } else { 0.4 };
|
||
out.push((
|
||
"agent.tool.call",
|
||
json!({ "agentId": agent_id, "tool": tool, "target": target }),
|
||
));
|
||
out.push(("node.activity", json!({ "nodeId": node_id, "label": tool, "kind": "service", "heat": if file_op { 1.0 } else { 0.9 } })));
|
||
// the agent converges on the tool it's using (the Gource beam)
|
||
out.push(("world.touch", json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service", "weight": weight })));
|
||
|
||
// File-op tools ALSO emit a `file:<path>` touch so the repo
|
||
// detail view can build the tree from real events. The path
|
||
// comes from the input's `path` / `target` / `file` / `url`
|
||
// keys — same lookup summarize_input does but we keep the
|
||
// full string so the client can build the dir hierarchy.
|
||
if file_op {
|
||
if let Some(input) = payload.get("input") {
|
||
for k in ["path", "target", "file", "filename", "url"] {
|
||
if let Some(p) = input.get(k).and_then(|v| v.as_str()) {
|
||
let cleaned = p.trim().trim_start_matches("./");
|
||
if !cleaned.is_empty() {
|
||
let file_node = format!("file:{cleaned}");
|
||
out.push((
|
||
"node.activity",
|
||
json!({ "nodeId": file_node, "label": cleaned, "kind": "service", "heat": 1.0 }),
|
||
));
|
||
out.push((
|
||
"world.touch",
|
||
json!({ "agentId": agent_id, "nodeId": file_node, "kind": "service", "weight": 1.0 }),
|
||
));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
"approval_required" => {
|
||
let action = payload
|
||
.get("action_type")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("action");
|
||
let category = payload
|
||
.get("category")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("");
|
||
let door_id = payload
|
||
.get("approval_id")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("");
|
||
out.push((
|
||
"door.request",
|
||
json!({ "doorId": door_id, "agentId": agent_id, "action": action, "target": category, "summary": action }),
|
||
));
|
||
}
|
||
"agent_message" => {
|
||
let s = |k: &str| {
|
||
payload
|
||
.get(k)
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("")
|
||
.to_owned()
|
||
};
|
||
out.push((
|
||
"agent.message",
|
||
json!({
|
||
"fromAgentId": agent_id,
|
||
"toAgentId": s("to_agent_id"),
|
||
"toName": s("to_name"),
|
||
"text": s("text"),
|
||
"threadId": s("thread_id"),
|
||
}),
|
||
));
|
||
}
|
||
"room_message" => {
|
||
let s = |k: &str| {
|
||
payload
|
||
.get(k)
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("")
|
||
.to_owned()
|
||
};
|
||
let participant_ids = payload
|
||
.get("participant_ids")
|
||
.and_then(|v| v.as_array())
|
||
.map(|a| {
|
||
a.iter()
|
||
.filter_map(|x| x.as_str().map(|s| s.to_owned()))
|
||
.collect::<Vec<_>>()
|
||
})
|
||
.unwrap_or_default();
|
||
out.push((
|
||
"room.message",
|
||
json!({
|
||
"fromAgentId": agent_id,
|
||
"threadId": s("thread_id"),
|
||
"subject": s("subject"),
|
||
"text": s("text"),
|
||
"participantIds": participant_ids,
|
||
}),
|
||
));
|
||
}
|
||
"a2a_invoked" => {
|
||
// An external A2A caller started a turn on this agent (a new ingress).
|
||
out.push(("a2a.invoked", json!({ "agentId": agent_id })));
|
||
}
|
||
_ => {}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Count of doors (approvals) awaiting a decision in the workspace.
|
||
async fn doors_pending(pool: &PgPool, ws: WorkspaceId) -> i64 {
|
||
sqlx::query_scalar::<_, i64>(
|
||
"SELECT count(*) FROM approvals WHERE workspace_id = $1 AND status = 'pending'",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_one(pool)
|
||
.await
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
/// Live per-agent metrics for the agent command center's metric band.
|
||
#[derive(Default)]
|
||
struct AgentTele {
|
||
tokens_per_min: i64,
|
||
cost_per_hr: f64,
|
||
loops: i64,
|
||
doors: i64,
|
||
}
|
||
|
||
/// Per-agent telemetry for every agent in the workspace, in 4 grouped queries
|
||
/// (not N×4): tokens over the last minute, credits over the last hour, active
|
||
/// routines, and pending approvals — all keyed by agent id.
|
||
async fn agent_telemetry(
|
||
pool: &PgPool,
|
||
ws: WorkspaceId,
|
||
) -> std::collections::HashMap<String, AgentTele> {
|
||
let mut m: std::collections::HashMap<String, AgentTele> = std::collections::HashMap::new();
|
||
let id_of = |r: &sqlx::postgres::PgRow| r.get::<uuid::Uuid, _>("agent_id").to_string();
|
||
|
||
// tokens in the last minute ≈ tokens/min.
|
||
for r in sqlx::query(
|
||
"SELECT agent_id, SUM(tokens_in + tokens_out)::bigint AS v FROM usage_events
|
||
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 minute'
|
||
GROUP BY agent_id",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default()
|
||
{
|
||
m.entry(id_of(&r)).or_default().tokens_per_min = r.get("v");
|
||
}
|
||
// credits spent in the last hour ≈ spend/hr.
|
||
for r in sqlx::query(
|
||
"SELECT agent_id, SUM(credits)::float8 AS v FROM usage_events
|
||
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 hour'
|
||
GROUP BY agent_id",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default()
|
||
{
|
||
m.entry(id_of(&r)).or_default().cost_per_hr = r.get("v");
|
||
}
|
||
// active routines = loops.
|
||
for r in sqlx::query(
|
||
"SELECT r.agent_id AS agent_id, count(*)::bigint AS v FROM routines r
|
||
JOIN agents a ON a.id = r.agent_id
|
||
WHERE a.workspace_id = $1 AND r.status = 'active' GROUP BY r.agent_id",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default()
|
||
{
|
||
m.entry(id_of(&r)).or_default().loops = r.get("v");
|
||
}
|
||
// pending approvals = doors.
|
||
for r in sqlx::query(
|
||
"SELECT requested_by_agent AS agent_id, count(*)::bigint AS v FROM approvals
|
||
WHERE workspace_id = $1 AND status = 'pending' GROUP BY requested_by_agent",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_all(pool)
|
||
.await
|
||
.unwrap_or_default()
|
||
{
|
||
m.entry(id_of(&r)).or_default().doors = r.get("v");
|
||
}
|
||
m
|
||
}
|
||
|
||
/// Query for `GET /api/world/live`.
|
||
#[derive(serde::Deserialize)]
|
||
pub struct LiveQuery {
|
||
/// Scope the feed to one mission. The client already filters events by
|
||
/// mission, but doing it here means the server stops querying and sending
|
||
/// what the viewer will discard.
|
||
pub mission: Option<Uuid>,
|
||
}
|
||
|
||
/// `GET /api/world/live` — the taxonomy SSE feed.
|
||
pub async fn world_live(
|
||
State(state): State<AppState>,
|
||
Authed(user): Authed,
|
||
Query(q): Query<LiveQuery>,
|
||
) -> impl IntoResponse {
|
||
let pool = state.pool.clone();
|
||
let ws = user.workspace_id;
|
||
let only_mission = q.mission;
|
||
|
||
let stream = async_stream::stream! {
|
||
let mut first = true;
|
||
// Remember last status per agent so we only push deltas after the seed.
|
||
let mut last: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||
// Per-run journal cursor so we stream only NEW run_events each poll.
|
||
let mut cursors: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||
// Last emitted signature per mission / per phase, so the plan is sent
|
||
// once and then only when something actually moves.
|
||
let mut last_mission: std::collections::HashMap<String, String> =
|
||
std::collections::HashMap::new();
|
||
let mut last_phase: std::collections::HashMap<String, String> =
|
||
std::collections::HashMap::new();
|
||
// (phase, path) pairs already announced — a delivered file is a fact
|
||
// that happened once, not a recurring event.
|
||
let mut last_file: HashSet<String> = HashSet::new();
|
||
// `mission_events` cursor. -1 until the first pass seeds it, which is
|
||
// what separates BACKFILL from MOTION: everything already in the table
|
||
// when a subscriber arrives is history and is drawn as a settled map,
|
||
// and only what lands afterwards is animated. Without the distinction,
|
||
// opening a finished mission would replay an hour of tool calls as a
|
||
// burst storm and read as a mission that just did all of it at once.
|
||
let mut event_cursor: i64 = -1;
|
||
// Findings already announced. A finding is raised once.
|
||
let mut last_finding: HashSet<String> = HashSet::new();
|
||
// Last benchmark summary per phase; a looping phase produces a new one.
|
||
let mut last_bench: std::collections::HashMap<String, String> =
|
||
std::collections::HashMap::new();
|
||
// Audit-log cursor for edge-initiated inter-agent events (delegation,
|
||
// A2A) that bypass the run loop. -1 until seeded on the first pass.
|
||
let mut audit_cursor: i64 = -1;
|
||
// Track the brain-file size we last announced per agent so we only
|
||
// emit `agent.memory` when the file has actually grown (or shrunk).
|
||
// On first seed we still emit — the World engine needs the initial
|
||
// scale for every pawn.
|
||
let mut last_bytes: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
|
||
loop {
|
||
let roster = match cm_db::repo::agents::roster(&pool, ws).await {
|
||
Ok(r) => r,
|
||
Err(_) => break,
|
||
};
|
||
let mut working = working_agents(&pool, ws).await;
|
||
let runs = active_runs(&pool, ws).await;
|
||
for (_, agent_id) in &runs {
|
||
working.insert(agent_id.clone());
|
||
}
|
||
|
||
if first {
|
||
// Seed the world graph with the workspace's agents as nodes.
|
||
let nodes: Vec<Value> = roster
|
||
.iter()
|
||
.map(|a| json!({ "id": a.id.to_string(), "tier": "agent", "label": a.name }))
|
||
.collect();
|
||
yield sse("topology.update", json!({ "formation": "live", "nodes": nodes }));
|
||
}
|
||
|
||
for a in &roster {
|
||
let id = a.id.to_string();
|
||
let status = if working.contains(&id) { "working" } else { "idle" };
|
||
if last.get(&id).map(|s| s != status).unwrap_or(true) {
|
||
last.insert(id.clone(), status.to_string());
|
||
yield sse(
|
||
"agent.status",
|
||
json!({ "agentId": id, "status": status, "role": a.job_title }),
|
||
);
|
||
}
|
||
// Cheap brain-file stat — just reads the inode metadata, no
|
||
// HDF5 open. Missing file (never provisioned) → treat as 0
|
||
// so the pawn stays at its base size. Only emit on change.
|
||
let brain_path = crate::routes::claws::brain_dir()
|
||
.join(format!("claw_{}.h5", id));
|
||
let bytes = std::fs::metadata(&brain_path).ok().map(|m| m.len()).unwrap_or(0);
|
||
if last_bytes.get(&id).map(|&b| b != bytes).unwrap_or(true) {
|
||
last_bytes.insert(id.clone(), bytes);
|
||
yield sse("agent.memory", json!({ "agentId": id, "bytes": bytes }));
|
||
}
|
||
}
|
||
|
||
// Mission landmarks, and the whole phase plan behind them.
|
||
//
|
||
// The orb pins "this is the work"; the phases give it shape. Every
|
||
// phase is emitted, including ones that have not started, because
|
||
// the World draws the plan upfront and lights it as it progresses —
|
||
// a phase list that appeared only as phases began would make a
|
||
// five-phase mission indistinguishable from a one-phase mission
|
||
// until it was nearly over.
|
||
let missions = world_missions(&pool, ws, only_mission).await;
|
||
let mut seen_missions: HashSet<String> = HashSet::new();
|
||
for m in &missions {
|
||
if seen_missions.insert(m.mission_id.clone()) {
|
||
// Delta-guarded like every other stateful event here. Without
|
||
// this it re-sent the same mission on every poll — harmless
|
||
// to a client that keys on missionId, and pure noise on the
|
||
// wire that hides the events that DID change.
|
||
let sig = format!("{}|{}", m.status, m.completed_at.as_deref().unwrap_or(""));
|
||
if last_mission.get(&m.mission_id).map(|s| s != &sig).unwrap_or(true) {
|
||
last_mission.insert(m.mission_id.clone(), sig);
|
||
yield sse(
|
||
"mission.update",
|
||
json!({
|
||
"missionId": m.mission_id,
|
||
"title": m.title,
|
||
"status": m.status,
|
||
"templateKind": m.template_kind,
|
||
"completedAt": m.completed_at,
|
||
}),
|
||
);
|
||
}
|
||
// A terminal mission is a map to read, not a scene to
|
||
// animate: it still gets an orb, but no heat.
|
||
let running = m.status == "running";
|
||
yield sse(
|
||
"node.activity",
|
||
json!({
|
||
"nodeId": format!("mission:{}", m.mission_id),
|
||
"label": m.title,
|
||
"kind": "mission",
|
||
"heat": if running { 0.75 } else { 0.0 },
|
||
}),
|
||
);
|
||
}
|
||
// `agent_id` is None for a teamless microVM mission. No pawn
|
||
// beams at it, which is accurate — nothing on this platform ran
|
||
// that phase except a VM — and is why the join had to become a
|
||
// LEFT JOIN rather than being left to drop the mission entirely.
|
||
if let (Some(agent_id), true) = (&m.agent_id, m.status == "running") {
|
||
yield sse(
|
||
"world.touch",
|
||
json!({
|
||
"agentId": agent_id,
|
||
"nodeId": format!("mission:{}", m.mission_id),
|
||
"kind": "mission",
|
||
"weight": 0.6,
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
for p in world_phases(&pool, ws, only_mission).await {
|
||
// Re-emit only on change. `iteration` and `completed_at` are in
|
||
// the signature because a phase that loops re-enters `running`
|
||
// from `running` — status alone would hide the retry.
|
||
let sig = format!(
|
||
"{}|{}|{}",
|
||
p.status,
|
||
p.iteration,
|
||
p.completed_at.as_deref().unwrap_or("")
|
||
);
|
||
let changed = last_phase.get(&p.phase_id).map(|s| s != &sig).unwrap_or(true);
|
||
if !changed {
|
||
continue;
|
||
}
|
||
last_phase.insert(p.phase_id.clone(), sig);
|
||
let agents = phase_agents(&pool, &p.mission_id, &p.kind).await;
|
||
yield sse(
|
||
"mission.phase",
|
||
json!({
|
||
"missionId": p.mission_id,
|
||
"phaseId": p.phase_id,
|
||
"kind": p.kind,
|
||
"orderIdx": p.order_idx,
|
||
"status": p.status,
|
||
"iteration": p.iteration,
|
||
"startedAt": p.started_at,
|
||
"completedAt": p.completed_at,
|
||
"agentIds": agents,
|
||
}),
|
||
);
|
||
yield sse(
|
||
"node.activity",
|
||
json!({
|
||
"nodeId": format!("phase:{}", p.phase_id),
|
||
"label": p.kind,
|
||
"kind": "phase",
|
||
// The client owns the lit/dim encoding via `status`;
|
||
// heat here is only the arrival pulse.
|
||
"heat": if p.status == "running" { 0.8 } else { 0.0 },
|
||
}),
|
||
);
|
||
}
|
||
|
||
// Files come AFTER the phases they belong to, deliberately: the
|
||
// client hangs a file tree under the coding station, and parenting
|
||
// there is first-write-wins. A file that arrived before its plan
|
||
// would pin its whole directory tree at the origin.
|
||
//
|
||
// Files, once each. A file is emitted when its phase's diff was
|
||
// captured and never again — the World keeps the node alive itself,
|
||
// and re-sending would re-burst the orb every poll as though the
|
||
// file had just been touched again.
|
||
for f in world_files(&pool, ws, only_mission).await {
|
||
let key = format!("{}|{}", f.phase_id, f.path);
|
||
if last_file.contains(&key) {
|
||
continue;
|
||
}
|
||
last_file.insert(key);
|
||
yield sse(
|
||
"mission.file",
|
||
json!({
|
||
"missionId": f.mission_id,
|
||
"phaseId": f.phase_id,
|
||
"path": f.path,
|
||
"status": f.status,
|
||
"source": "diff",
|
||
}),
|
||
);
|
||
}
|
||
|
||
// Benchmark results, as an annotation on the station. Re-emitted
|
||
// only when the summary changes: a phase that loops produces a new
|
||
// snapshot per iteration, and that IS news.
|
||
for b in world_benchmarks(&pool, ws, only_mission).await {
|
||
if last_bench.get(&b.phase_id).map(|n| n == &b.note).unwrap_or(false) {
|
||
continue;
|
||
}
|
||
last_bench.insert(b.phase_id.clone(), b.note.clone());
|
||
yield sse(
|
||
"mission.benchmark",
|
||
json!({
|
||
"missionId": b.mission_id,
|
||
"phaseId": b.phase_id,
|
||
"note": b.note,
|
||
}),
|
||
);
|
||
}
|
||
|
||
// Findings, once each, hanging off the security station. Same rule
|
||
// as files: a finding is a fact that was raised once, and
|
||
// re-sending it would pop the orb again on every poll.
|
||
for f in world_findings(&pool, ws, only_mission).await {
|
||
if !last_finding.insert(f.task_id.clone()) {
|
||
continue;
|
||
}
|
||
yield sse(
|
||
"mission.finding",
|
||
json!({
|
||
"missionId": f.mission_id,
|
||
"phaseId": f.phase_id,
|
||
"findingId": f.task_id,
|
||
"title": f.title,
|
||
}),
|
||
);
|
||
}
|
||
|
||
// Structured activity — the motion channel. Tool calls and file
|
||
// touches recorded at the source by the container tap and the
|
||
// microVM `PostToolUse` hook. Never parsed from prose: a tool name
|
||
// in a log is indistinguishable from an agent TALKING about a tool.
|
||
{
|
||
let backfill = event_cursor < 0;
|
||
for a in world_acts(&pool, ws, only_mission, event_cursor.max(0)).await {
|
||
event_cursor = event_cursor.max(a.id);
|
||
match a.kind.as_str() {
|
||
"file.touch" => {
|
||
let key = format!("{}|{}", a.phase_id.as_deref().unwrap_or(""), a.target);
|
||
if !last_file.insert(key) {
|
||
continue;
|
||
}
|
||
yield sse(
|
||
"mission.file",
|
||
json!({
|
||
"missionId": a.mission_id,
|
||
"phaseId": a.phase_id,
|
||
"agentId": a.agent_id,
|
||
"path": a.target,
|
||
// Backlog is history: it draws the file and
|
||
// stops there. Only what lands while someone
|
||
// is watching is motion.
|
||
"source": if backfill { "diff" } else { "tool" },
|
||
}),
|
||
);
|
||
}
|
||
// A tool call with an agent is a pawn leaving its
|
||
// station and coming back; without one (a microVM phase
|
||
// has no platform agent) it is only a node lighting up,
|
||
// which is the truth rather than an invented traveller.
|
||
_ if !backfill => {
|
||
let node_id = format!("tool:{}", a.target);
|
||
match &a.agent_id {
|
||
Some(agent) => yield sse(
|
||
"world.touch",
|
||
json!({
|
||
"agentId": agent,
|
||
"nodeId": node_id,
|
||
"kind": "event",
|
||
"weight": 0.45,
|
||
}),
|
||
),
|
||
None => yield sse(
|
||
"node.activity",
|
||
json!({
|
||
"nodeId": node_id,
|
||
"label": a.target,
|
||
"kind": "event",
|
||
"heat": 0.5,
|
||
}),
|
||
),
|
||
}
|
||
}
|
||
// A backlogged tool call draws nothing: the orb would be
|
||
// a tool nobody is using, permanently lit on a map of
|
||
// work that already finished.
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Real convergence: each running agent beams toward its active-run node.
|
||
for (run_id, agent_id) in &runs {
|
||
let node_id = format!("run:{}", &run_id[..run_id.len().min(8)]);
|
||
yield sse(
|
||
"node.activity",
|
||
json!({ "nodeId": node_id, "label": "active run", "kind": "event", "heat": 0.85 }),
|
||
);
|
||
yield sse("world.touch", json!({ "agentId": agent_id, "nodeId": node_id, "kind": "event" }));
|
||
|
||
// Tail the run's journal for richer real events (reasoning, tool
|
||
// convergence, doors). On first sight, jump the cursor to the
|
||
// current max so we stream forward without replaying the backlog.
|
||
//
|
||
// THIS BLOCK IS THE a2a/door LAYER ONLY, and it works: those
|
||
// runs are the ones that write `run_events` (`run_events::append`
|
||
// is reached from `routes/a2a.rs` and `mcp_door.rs`).
|
||
//
|
||
// A sibling block used to read `topology_runs.checkpoint.records`
|
||
// here, on the theory that it covered missions. It could never
|
||
// have: `run_id` comes from `active_runs`, which selects
|
||
// `agent_runs.id`, and mission phases live in `topology_runs`
|
||
// under independently generated ids. The query ran every poll and
|
||
// matched nothing, for every mission, forever — which is why the
|
||
// World has never shown a mission's tool or file activity.
|
||
// Removed rather than repointed: pointing it at `topology_runs`
|
||
// would resurrect a path whose only per-step content is the
|
||
// agent's own prose output, and a tool name in prose is
|
||
// indistinguishable from an agent talking about a tool. Mission
|
||
// detail arrives as structured `mission_events` instead.
|
||
if let Some(&after) = cursors.get(run_id) {
|
||
let rows = sqlx::query(
|
||
"SELECT seq, event_type, payload FROM run_events
|
||
WHERE run_id = $1::uuid AND seq > $2 ORDER BY seq ASC LIMIT 200",
|
||
)
|
||
.bind(run_id)
|
||
.bind(after)
|
||
.fetch_all(&pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
let mut maxseq = after;
|
||
for row in &rows {
|
||
let seq: i64 = row.get("seq");
|
||
let et: String = row.get("event_type");
|
||
let payload: Value = row.get("payload");
|
||
for (t, d) in normalize_run_event(agent_id, &et, &payload) {
|
||
yield sse(t, d);
|
||
}
|
||
if seq > maxseq {
|
||
maxseq = seq;
|
||
}
|
||
}
|
||
cursors.insert(run_id.clone(), maxseq);
|
||
} else {
|
||
let maxseq: i64 = sqlx::query_scalar(
|
||
"SELECT coalesce(max(seq), -1) FROM run_events WHERE run_id = $1::uuid",
|
||
)
|
||
.bind(run_id)
|
||
.fetch_one(&pool)
|
||
.await
|
||
.unwrap_or(-1);
|
||
cursors.insert(run_id.clone(), maxseq);
|
||
}
|
||
}
|
||
|
||
// Per-agent telemetry → the command-center metric band (real data:
|
||
// usage_events tokens/credits, active routines, pending approvals).
|
||
let tele = agent_telemetry(&pool, ws).await;
|
||
for a in &roster {
|
||
let id = a.id.to_string();
|
||
let t = tele.get(&id);
|
||
yield sse("telemetry", json!({
|
||
"agentId": id,
|
||
"tokensPerMin": t.map(|x| x.tokens_per_min).unwrap_or(0),
|
||
"costPerHr": t.map(|x| x.cost_per_hr).unwrap_or(0.0),
|
||
"loops": t.map(|x| x.loops).unwrap_or(0),
|
||
"doorsPending": t.map(|x| x.doors).unwrap_or(0),
|
||
}));
|
||
}
|
||
|
||
// Workspace-wide telemetry (top-bar pills / Observe system strip).
|
||
yield sse(
|
||
"telemetry",
|
||
json!({ "doorsPending": doors_pending(&pool, ws).await, "loops": runs.len() }),
|
||
);
|
||
|
||
// Edge-initiated inter-agent events (gated delegation, A2A ingress)
|
||
// bypass the run loop, so surface them from the append-only audit log.
|
||
// On first sight jump the cursor to the current max so we stream
|
||
// forward instead of replaying history.
|
||
if audit_cursor < 0 {
|
||
audit_cursor = sqlx::query_scalar(
|
||
"SELECT coalesce(max(id), 0) FROM audit_log WHERE workspace_id = $1",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.fetch_one(&pool)
|
||
.await
|
||
.unwrap_or(0);
|
||
} else {
|
||
let rows = sqlx::query(
|
||
"SELECT id, actor_id, event_type, subject_id, detail FROM audit_log
|
||
WHERE workspace_id = $1 AND id > $2
|
||
AND event_type IN ('delegation.invoked', 'a2a.invoked')
|
||
ORDER BY id ASC LIMIT 100",
|
||
)
|
||
.bind(ws.as_uuid())
|
||
.bind(audit_cursor)
|
||
.fetch_all(&pool)
|
||
.await
|
||
.unwrap_or_default();
|
||
for row in &rows {
|
||
let id: i64 = row.get("id");
|
||
let et: String = row.get("event_type");
|
||
let actor: Option<uuid::Uuid> = row.get("actor_id");
|
||
let subject: String = row.get("subject_id");
|
||
let detail: Value = row.get("detail");
|
||
match et.as_str() {
|
||
"delegation.invoked" => {
|
||
yield sse("agent.delegate", json!({
|
||
"fromAgentId": actor.map(|u| u.to_string()).unwrap_or_default(),
|
||
"toAgentId": detail.get("to_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||
"toName": subject,
|
||
"task": detail.get("task").and_then(|v| v.as_str()).unwrap_or(""),
|
||
}));
|
||
}
|
||
"a2a.invoked" => {
|
||
// subject_id is the claw_<id> alias → surface the target agent.
|
||
let agent_id = subject
|
||
.strip_prefix("claw_")
|
||
.and_then(|h| uuid::Uuid::parse_str(h).ok())
|
||
.map(|u| u.to_string())
|
||
.unwrap_or_else(|| subject.clone());
|
||
yield sse("a2a.invoked", json!({ "agentId": agent_id }));
|
||
}
|
||
_ => {}
|
||
}
|
||
if id > audit_cursor {
|
||
audit_cursor = id;
|
||
}
|
||
}
|
||
}
|
||
|
||
first = false;
|
||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||
}
|
||
};
|
||
|
||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
pub struct ReplayQuery {
|
||
hours: Option<i64>,
|
||
}
|
||
|
||
/// `GET /api/world/replay?hours=24` — a Gource-style timeline reconstructed from
|
||
/// the workspace's run history: a sorted list of timestamped taxonomy events the
|
||
/// client's WorldClock plays back into the same engine (live + replay).
|
||
pub async fn world_replay(
|
||
State(state): State<AppState>,
|
||
Authed(user): Authed,
|
||
Query(q): Query<ReplayQuery>,
|
||
) -> Result<Json<Value>, ApiError> {
|
||
let hours = q.hours.unwrap_or(24).clamp(1, 720);
|
||
let rows = sqlx::query(
|
||
"SELECT ar.id::text AS run_id, s.agent_id::text AS agent_id,
|
||
extract(epoch FROM ar.created_at)::float8 AS started
|
||
FROM agent_runs ar JOIN sessions s ON s.id = ar.session_id
|
||
WHERE s.workspace_id = $1 AND ar.created_at > now() - ($2 * interval '1 hour')
|
||
ORDER BY ar.created_at ASC",
|
||
)
|
||
.bind(user.workspace_id.as_uuid())
|
||
.bind(hours)
|
||
.fetch_all(&state.pool)
|
||
.await?;
|
||
|
||
let mut events: Vec<Value> = Vec::new();
|
||
for r in &rows {
|
||
let run_id: String = r.get("run_id");
|
||
let agent_id: String = r.get("agent_id");
|
||
let started: f64 = r.get("started");
|
||
let node_id = format!("run:{}", &run_id[..run_id.len().min(8)]);
|
||
events.push(json!({ "t": started, "type": "agent.status", "data": { "agentId": agent_id, "status": "working" }}));
|
||
events.push(json!({ "t": started, "type": "node.activity", "data": { "nodeId": node_id, "label": "run", "kind": "event", "heat": 0.85 }}));
|
||
events.push(json!({ "t": started, "type": "world.touch", "data": { "agentId": agent_id, "nodeId": node_id, "kind": "event" }}));
|
||
}
|
||
Ok(Json(
|
||
json!({ "events": events, "hours": hours, "count": rows.len() }),
|
||
))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod mission_feed_tests {
|
||
/// The mission query must not re-acquire any of the three bugs it fixed.
|
||
///
|
||
/// All three were invisible: an INNER JOIN does not error when it drops a
|
||
/// row, the legacy `missions.team_id` is a real column that resolves fine,
|
||
/// and `status = 'running'` is a defensible-looking filter. The World simply
|
||
/// showed less than the truth and nothing anywhere said so. A source walk is
|
||
/// the only guard available — these are runtime `sqlx::query` calls, so the
|
||
/// compiler checks nothing about them.
|
||
#[test]
|
||
fn the_mission_query_keeps_teamless_missions() {
|
||
let src = include_str!("world.rs");
|
||
let q = src
|
||
.split("async fn world_missions")
|
||
.nth(1)
|
||
.and_then(|s| s.split("async fn").next())
|
||
.expect("world_missions body");
|
||
|
||
assert!(
|
||
q.contains("LEFT JOIN mission_teams"),
|
||
"must LEFT JOIN mission_teams: a microVM mission has no team rows at \
|
||
all, and an INNER JOIN silently drops the platform's primary \
|
||
execution tier"
|
||
);
|
||
assert!(
|
||
q.contains("LEFT JOIN team_members"),
|
||
"the second join must be LEFT too, or the mission reappears and its \
|
||
agents take it back out"
|
||
);
|
||
assert!(
|
||
!q.contains("tm.team_id = m.team_id"),
|
||
"must not join on the legacy missions.team_id — 0056 superseded it \
|
||
with mission_teams, and it points at only the FIRST minted team"
|
||
);
|
||
assert!(
|
||
q.contains("m.completed_at >"),
|
||
"recently-finished missions must be included or the World is empty \
|
||
almost always: missions finish in minutes"
|
||
);
|
||
assert!(
|
||
q.contains("m.status") && q.contains("m.template_kind"),
|
||
"status and template_kind must be carried: one decides finished-map \
|
||
vs live, the other the palette"
|
||
);
|
||
}
|
||
|
||
/// The checkpoint tail read `topology_runs` keyed by an `agent_runs` id and
|
||
/// could never match. If it comes back, missions silently lose their tool
|
||
/// and file detail again — and the query looks entirely reasonable.
|
||
#[test]
|
||
fn the_dead_checkpoint_tail_stays_dead() {
|
||
let src = include_str!("world.rs");
|
||
// Split so this assertion's own literal is not what the source walk
|
||
// finds. Written whole, the test fails on itself — which it did, and
|
||
// which is the same self-match that makes `pkill -f <pattern>` kill the
|
||
// shell carrying the pattern.
|
||
let needle = concat!("SELECT checkpoint FROM topology_", "runs WHERE id = $1::uuid");
|
||
assert!(
|
||
!src.contains(needle),
|
||
"the checkpoint tail is keyed on an agent_runs id and cannot match a \
|
||
topology_runs row; mission detail comes from structured events"
|
||
);
|
||
}
|
||
|
||
/// A benchmark delta is described only as far as its shape allows.
|
||
///
|
||
/// `compute_delta` emits `{kind:"opaque"}` whenever the before/after
|
||
/// metrics were not structurally comparable — which is most drivers. The
|
||
/// temptation is to say something anyway; the result would be a performance
|
||
/// claim the picture makes and the data does not support.
|
||
#[test]
|
||
fn a_benchmark_is_described_only_as_far_as_its_shape_allows() {
|
||
use serde_json::json;
|
||
assert_eq!(
|
||
super::benchmark_note(&json!({
|
||
"kind": "bencher_diff",
|
||
"samples": [
|
||
{"name": "a", "delta_pct": -12.5, "direction": "improved"},
|
||
{"name": "b", "delta_pct": 3.0, "direction": "regressed"},
|
||
// No direction and no percentage: counted, claimed for
|
||
// neither side.
|
||
{"name": "c"},
|
||
],
|
||
}))
|
||
.as_deref(),
|
||
Some("3 sample(s), 1 faster, 1 slower, best -12.5%")
|
||
);
|
||
// Nothing comparable happened, so nothing is said.
|
||
assert_eq!(
|
||
super::benchmark_note(&json!({ "kind": "opaque", "note": "not comparable" })),
|
||
None
|
||
);
|
||
assert_eq!(
|
||
super::benchmark_note(&json!({ "kind": "bencher_diff", "samples": [] })),
|
||
None
|
||
);
|
||
// Everything got slower: no "best" claim at all.
|
||
assert_eq!(
|
||
super::benchmark_note(&json!({
|
||
"samples": [{"name": "a", "delta_pct": 8.0, "direction": "regressed"}],
|
||
}))
|
||
.as_deref(),
|
||
Some("1 sample(s), 1 slower")
|
||
);
|
||
}
|
||
|
||
/// Agent→phase attribution must come from the runner's own mapping.
|
||
#[test]
|
||
fn phase_attribution_reuses_the_runners_mapping() {
|
||
let src = include_str!("world.rs");
|
||
assert!(
|
||
src.contains("crate::phase_runner::purposes_for"),
|
||
"a second copy of the kind→purpose mapping would let the picture \
|
||
disagree with the machine about who is working on what"
|
||
);
|
||
}
|
||
|
||
/// Files must be emitted after the phases they hang under.
|
||
///
|
||
/// The client parents a file's directory chain to the coding station, and
|
||
/// parenting is first-write-wins — so a file that reaches the browser
|
||
/// before its plan pins its whole tree at the origin permanently. Nothing
|
||
/// errors: the tree renders, in the wrong place, and reads as a layout
|
||
/// choice. Swapping the two loops back is a one-line-looking edit, which is
|
||
/// exactly why it needs a guard.
|
||
#[test]
|
||
fn files_are_emitted_after_the_phases_they_hang_under() {
|
||
let src = include_str!("world.rs");
|
||
let phases = src
|
||
.find("for p in world_phases(")
|
||
.expect("the phase emission loop");
|
||
let files = src
|
||
.find("for f in world_files(")
|
||
.expect("the file emission loop");
|
||
assert!(
|
||
phases < files,
|
||
"the phase loop must run before the file loop; files parented \
|
||
before their coding station stay at the origin forever"
|
||
);
|
||
}
|
||
}
|