world viz: agents grow with their brain (log-curve dot size)
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 2m38s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m40s

Fresh agents start at "size of their letters" — a small dot in
the live viz — and visibly bloom out as their .brain file fills.
Turns "which of these agents is heavily loaded" into a glance
instead of a menu dive.

Taxonomy
- New agent.memory event: { agentId, bytes?, count? }. STATEFUL, so
  a late subscriber sees the last value replayed and pawns arrive
  pre-sized. count is included in the schema for a future combo
  metric but not emitted yet — bytes carries the visual today.

Backend (routes/world.rs SSE loop)
- Per-agent, per-tick std::fs::metadata() on
  brain_dir()/claw_<uuid>.h5. Just the inode stat — no HDF5 open,
  no memory count, sub-ms per agent. Emit agent.memory { bytes }
  only when the value has changed (or on first sight).
- Tracks last_bytes: HashMap<String, u64> in the SSE-stream scope
  alongside the existing status HashMap.
- Missing file (agent never provisioned a brain) reads as 0 bytes
  and yields scale = 1.0 downstream — pawn stays small.

Engine
- GPawn gains memoryScale (visible) + memoryScaleTarget (chased).
  Base is 1.0; ensurePawn initializes both.
- memoryScaleFromBytes(bytes): 1 + log10(1 + bytes/1MB) * 0.6, cap
  MAX_MEMORY_SCALE = 3.5. So 10MB ~ 1.6x, 100MB ~ 2.2x, 1GB ~ 2.8x.
  Log curve keeps a heavy brain readable without a lite one being
  invisible.
- onMemory(e) sets the target. stepPawns eases the visible scale
  toward it at ~3/sec — a big incoming snapshot doesn't pop the
  sphere; it swells in like it's inhaling.

Renderer (WorldCanvas)
- Live subscription registers agent.memory alongside the existing
  status/touch/reasoning listeners.
- Pawn sphere scale = 5 * p.memoryScale (was hardcoded 8). Halo
  scales in proportion (max(24, 4.25 * s)) so a memory-heavy agent
  reads as a bigger presence, not a small dot with a huge halo.
- AABB bounds for the frame-camera math updated to use s instead
  of 8 so the camera actually frames a big agent when it's the
  outlier.

Not yet wired: comm lines between pawns when agents talk to each
other (Commit E next), and the topology-edge overlay that renders
the graph shape dimly at rest. Both build on top of this — bigger
dots make comm beams more visible.
This commit is contained in:
Omar Sobh
2026-07-09 11:52:45 -07:00
parent aed21b654c
commit aa941b72a1
4 changed files with 69 additions and 7 deletions
+15
View File
@@ -287,6 +287,11 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
// 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,
@@ -317,6 +322,16 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
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 }));
}
}
// Real convergence: each running agent beams toward its active-run node.