world: loop:<id> landmark orbs (V1)
Mirror the repo: landmark pattern for scheduled loops. Every enabled loop in the workspace gets a labeled amber orb in the World, whether it's currently running or between fires. Assigned agents converge on it with a soft 0.15 touch — the loop is a persistent landmark, not a transient run. Backend: - active_loops(pool, ws) query joins loops + loop_agents where enabled, returning (loop_id, title, agent_id) — one row per (loop, agent). - SSE loop emits node.activity + world.touch symmetric to the research block. Seen-once set dedupes the label emission across agents. Frontend: - New "loop" tier in the Tier alias, LEVEL_COLOR (#f0b866 warm amber), and ensureNode radius (11 — same landmark size as repo). - engine.onTouch / onNodeActivity preserve the tier from the loop: prefix (previously would have collapsed to service). - Pawn fireColor tinted amber for loop: touches. - WorldCanvas: loop tier joins the struct group for solid-at-rest glow + always-on labels. Focus mode recognizes loop: prefix (click → focused subtree, Esc to exit). Focus pill switches to "LOOP FOCUS" in amber when the selected id is a loop. Contrast with repo: (transient — only appears when a topic is in processing/reviewing/publishing). Loops are persistent because their whole point is recurrence.
This commit is contained in:
@@ -92,6 +92,35 @@ async fn active_research_topics(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Enabled scheduled loops + their assigned agents. Same shape as
|
||||
/// `active_research_topics` — `(loop_id, title, agent_id)` per (loop, agent).
|
||||
/// Emitted as `loop:<loop_id>` landmark orbs so recurring/scheduled work is
|
||||
/// visible in the World at all times, not just while a run is mid-flight.
|
||||
/// Contrast with research topics (transient statuses processing/reviewing/
|
||||
/// publishing) — loops are persistent landmarks the user can click.
|
||||
async fn active_loops(pool: &PgPool, ws: WorkspaceId) -> Vec<(String, String, String)> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT l.id::text AS loop_id, l.title AS title, la.agent_id::text AS agent_id
|
||||
FROM loops l
|
||||
JOIN loop_agents la ON la.loop_id = l.id
|
||||
WHERE l.workspace_id = $1
|
||||
AND l.enabled = TRUE",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
rows.into_iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r.get::<String, _>("loop_id"),
|
||||
r.get::<String, _>("title"),
|
||||
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"] {
|
||||
@@ -413,6 +442,27 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
|
||||
);
|
||||
}
|
||||
|
||||
// Scheduled loops → landmark orbs, symmetric to research topics.
|
||||
// Persistent landmarks: emitted whenever a loop is enabled, so a
|
||||
// loop between fires still reads as an in-flight project. When
|
||||
// a loop actually runs, the topology_worker journals events
|
||||
// which the run-cursor block below picks up and heats the orb.
|
||||
let loops = active_loops(&pool, ws).await;
|
||||
let mut seen_loops = std::collections::HashSet::new();
|
||||
for (loop_id, title, agent_id) in &loops {
|
||||
let node_id = format!("loop:{loop_id}");
|
||||
if seen_loops.insert(loop_id.clone()) {
|
||||
yield sse(
|
||||
"node.activity",
|
||||
json!({ "nodeId": node_id, "label": title, "kind": "service", "heat": 0.0 }),
|
||||
);
|
||||
}
|
||||
yield sse(
|
||||
"world.touch",
|
||||
json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service", "weight": 0.15 }),
|
||||
);
|
||||
}
|
||||
|
||||
// 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)]);
|
||||
|
||||
Reference in New Issue
Block a user