1. edge-worker.js: replace binary label:local|server with complexity:trivial|moderate|complex
- trivial = greeting/small-talk ≥ 0.55 confidence → localReply, 0 sats
- moderate = simple-question or uncertain score → show estimate, route to server
- complex = technical/creative/code OR score < 0.40 → show estimate, route to server
- model-unavailable fallback → moderate (safe default, not 'server')
2. edge-worker-client.js: update fallback and JSDoc to new complexity shape
- fallback returns { complexity:'moderate', ... } instead of { label:'server', ... }
3. ui.js: triage driven by cls.complexity, not cls.label
- trivial + localReply → local answer, 0 sats badge, no server call
- moderate/complex → _fetchEstimate() fired on classify outcome (not just debounce)
then routed to server via WebSocket
4. session.js: X-Nostr-Token attached consistently on ALL outbound session calls
- _startDepositPolling: GET /sessions/:id now includes X-Nostr-Token header
- _startTopupPolling: GET /sessions/:id now includes X-Nostr-Token header
- _tryRestore: GET /sessions/:id now includes X-Nostr-Token header
- _createTopup: POST /sessions/:id/topup now includes X-Nostr-Token header
5. nostr-identity.js: _canSign flag tracks signing capability separately from pubkey
- initNostrIdentity sets _canSign=true only when NIP-07 or privkey is available
- npub-only discovery sets _pubkey but _canSign=false → prompt IS scheduled
- Prompt shown when !_pubkey || !_canSign (not just !_pubkey)
- Prompt click handlers set _canSign=true after connecting NIP-07 or generating key
- refreshToken only called when _pubkey && _canSign (avoids silent failures)
Timmy Tower World
A Three.js 3D visualization of the Timmy agent network. Agents appear as glowing icosahedra connected by lines, pulsing as they process jobs. A matrix-rain particle effect fills the background.
Quick start
npm install
npm run dev # Vite dev server with hot reload → http://localhost:5173
npm run build # Production bundle → dist/
npm run preview # Serve dist/ locally
Configuration
Set these in a .env.local file (not committed):
VITE_WS_URL=ws://localhost:8080/ws/agents
Leave VITE_WS_URL unset to run in offline/demo mode (agents animate but
receive no live updates).
Adding custom agents
Edit one file only: js/agent-defs.js
export const AGENT_DEFS = [
// existing agents …
{
id: 'zeta', // unique string — matches WebSocket message agentId
label: 'ZETA', // displayed in the 3D HUD
color: 0xff00aa, // hex integer (0xRRGGBB)
role: 'observer', // shown under the label sprite
direction: 'east', // cardinal facing direction (north/east/south/west)
x: 12, // world-space position (horizontal)
z: 0, // world-space position (depth)
},
];
Nothing else needs to change. agents.js reads positions from x/z,
and websocket.js reads colors and labels — both derive everything from
AGENT_DEFS.
Architecture
js/
├── agent-defs.js ← single source of truth: id, label, color, role, position
├── agents.js ← Three.js scene objects, animation loop
├── effects.js ← matrix rain particles, starfield
├── interaction.js ← OrbitControls (pan, zoom, rotate)
├── main.js ← entry point, rAF loop
├── ui.js ← DOM HUD overlay (FPS, agent states, chat)
└── websocket.js ← WebSocket reconnect, message dispatch
WebSocket protocol
The backend sends JSON messages on the agents channel:
type |
Fields | Effect |
|---|---|---|
agent_state |
agentId, state |
Update agent visual state |
job_started |
agentId, jobId |
Increment job counter, pulse |
job_completed |
agentId, jobId |
Decrement job counter |
chat |
agentId, text |
Append to chat panel |
Agent states: idle (dim pulse) · active (bright pulse + fast ring spin)