|
| 1 | +# KB 05: Dashboard |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Localhost web dashboard for real-time agent introspection. Serves at `http://0.0.0.0:8080`, accessible via Tailscale at `http://norisor:8080`. |
| 6 | + |
| 7 | +Built with `aiohttp` — runs as another coroutine in `asyncio.gather` alongside the cognitive loop, consolidation, and peripherals. Dashboard crash does not kill the agent. |
| 8 | + |
| 9 | +## Architecture |
| 10 | + |
| 11 | +``` |
| 12 | +AgentState (dataclass) |
| 13 | + created in main.py |
| 14 | + written by cognitive_loop (assigns attention, gut, safety, etc.) |
| 15 | + read by dashboard handlers |
| 16 | + conversation list is shared (same object reference) |
| 17 | + SSE broadcast via asyncio.Queue per subscriber |
| 18 | +
|
| 19 | +run_dashboard() |
| 20 | + aiohttp web.Application |
| 21 | + binds 0.0.0.0:8080 |
| 22 | + waits on shutdown_event |
| 23 | + access_log disabled |
| 24 | +``` |
| 25 | + |
| 26 | +## Data Sharing |
| 27 | + |
| 28 | +`AgentState` is created in `main.py` and passed to both `cognitive_loop()` and `run_dashboard()`. |
| 29 | + |
| 30 | +- Loop assigns internal objects after creation: `agent_state.attention = attention`, etc. |
| 31 | +- Conversation list is shared by reference: `conversation = agent_state.conversation` |
| 32 | +- Exchange count synced: `agent_state.exchange_count = exchange_count` |
| 33 | +- All reads are safe (single event loop, no thread contention). |
| 34 | + |
| 35 | +## SSE Events |
| 36 | + |
| 37 | +The cognitive loop publishes events at 4 points via `agent_state.publish_event()`: |
| 38 | + |
| 39 | +| Event | When | Data | |
| 40 | +|-------|------|------| |
| 41 | +| `cycle_start` | After winner selected | source, content preview, salience, queue_size | |
| 42 | +| `llm_response` | After LLM response | reply preview, confidence, escalated | |
| 43 | +| `escalation` | Before System 2 call | triggers, confidence | |
| 44 | +| `gate_flush` | After periodic flush | persisted count, dropped count | |
| 45 | + |
| 46 | +Each browser tab gets its own `asyncio.Queue(maxsize=200)`. Events are fire-and-forget: `QueueFull` silently drops the subscriber. SSE keepalive every 15s. |
| 47 | + |
| 48 | +## API Routes |
| 49 | + |
| 50 | +``` |
| 51 | +GET / -> HTML dashboard (inline, dark theme) |
| 52 | +GET /events -> SSE stream (real-time cognitive events) |
| 53 | +GET /api/status -> JSON agent state snapshot |
| 54 | +GET /api/memories -> JSON paginated memory list (?limit=20&offset=0) |
| 55 | +GET /api/memory/{id} -> JSON single memory detail |
| 56 | +GET /api/attention -> JSON attention queue contents |
| 57 | +GET /api/gut -> JSON gut feeling state + delta log |
| 58 | +GET /api/conversation -> JSON current conversation window |
| 59 | +GET /api/energy -> JSON energy tracker breakdown |
| 60 | +``` |
| 61 | + |
| 62 | +## Memory Browser |
| 63 | + |
| 64 | +Uses direct `asyncpg pool.fetch()` with SELECT queries. Does NOT go through `MemoryStore` methods to avoid side effects: |
| 65 | +- No access count updates |
| 66 | +- No retrieval mutation |
| 67 | +- The agent doesn't "feel" you browsing its memories |
| 68 | + |
| 69 | +## Frontend |
| 70 | + |
| 71 | +Single inline HTML/CSS/JS string (`DASHBOARD_HTML`). Dark theme, vanilla JS with `EventSource`. |
| 72 | + |
| 73 | +4 panels: |
| 74 | +1. **Live Feed** - scrolling SSE events (attention wins, LLM responses, gate flushes, escalations) |
| 75 | +2. **Agent Status** - refreshes every 5s (phase, model, memory count, bootstrap, gut, energy cost) |
| 76 | +3. **Context Window** - refreshes every 3s (current conversation messages) |
| 77 | +4. **Memory Store** - paginated table with click-to-expand modal (refreshes every 10s) |
| 78 | + |
| 79 | +All dynamic content uses `textContent` and DOM construction (no `innerHTML` with API data) to prevent XSS. |
| 80 | + |
| 81 | +## Modules |
| 82 | + |
| 83 | +### `src/dashboard.py` (~900 lines) |
| 84 | + |
| 85 | +- `AgentState` dataclass with SSE broadcast |
| 86 | +- Route handlers for all API endpoints |
| 87 | +- `run_dashboard()` coroutine |
| 88 | +- Inline HTML/CSS/JS |
| 89 | + |
| 90 | +### `src/loop.py` (modified) |
| 91 | + |
| 92 | +- Signature: `cognitive_loop(..., agent_state=None)` |
| 93 | +- Assigns objects to `agent_state` after creation |
| 94 | +- Uses shared conversation: `agent_state.conversation if agent_state else []` |
| 95 | +- Publishes 4 SSE events at key points |
| 96 | +- All agent_state operations guarded by `if agent_state:` (no-op without dashboard) |
| 97 | + |
| 98 | +### `src/main.py` (modified) |
| 99 | + |
| 100 | +- Creates `AgentState(config=config, layers=layers, memory=memory)` |
| 101 | +- Passes `agent_state` to `cognitive_loop()` |
| 102 | +- Adds `run_dashboard(agent_state, shutdown_event)` to tasks |
| 103 | + |
| 104 | +## Security |
| 105 | + |
| 106 | +- Binds to 0.0.0.0 but only accessible via Tailscale (not exposed to public internet) |
| 107 | +- Read-only: no mutation endpoints, no write operations |
| 108 | +- No authentication (Tailscale provides network-level auth) |
| 109 | +- XSS prevented: all dynamic content via textContent/DOM construction |
| 110 | + |
| 111 | +## Port |
| 112 | + |
| 113 | +| Service | Port | |
| 114 | +|---------|------| |
| 115 | +| Dashboard | 8080 (mapped in docker-compose.yml) | |
| 116 | + |
| 117 | +## Resilience |
| 118 | + |
| 119 | +- `run_dashboard()` catches exceptions internally |
| 120 | +- Dashboard crash logs error and exits without calling `shutdown_event.set()` |
| 121 | +- Agent continues operating via Telegram/stdin without dashboard |
| 122 | +- `agent_state=None` default means loop works identically without dashboard |
0 commit comments