world viz: agents grow with their brain (log-curve dot size)
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user