Addresses all code review rejections:
1. edge-worker.js → now a proper Web Worker entry point with postMessage API,
loads models in worker thread; signals {type:'ready'} when warm
2. edge-worker-client.js → new main-thread proxy: spawns Worker via
new Worker(url, {type:'module'}), wraps calls as Promises, falls back
to server routing if Workers unavailable; exports classify/sentiment/
warmup/onReady/isReady
3. nostr-identity.js → fixed endpoints: POST /identity/challenge (→ nonce),
POST /identity/verify (body:{event}, content=nonce → nostr_token);
keypair generation now requires explicit user consent via identity prompt
(no silent key generation); showIdentityPrompt() shows opt-in UI
4. ui.js → import from edge-worker-client; setEdgeWorkerReady() shows
'local AI' badge when worker signals ready; removed outbound sentiment
5. websocket.js → sentiment() on inbound Timmy chat messages drives setMood()
6. session.js → sentiment() on inbound reply (data.result), not outbound text
7. main.js → onEdgeWorkerReady(() => setEdgeWorkerReady()) wires ready badge
8. vite.config.js → worker.format:'es' for ESM Web Worker bundling
Implement ragdoll physics for agent interactions, including a state machine for falling, getting up, and counter-attacks. Introduce camera shake based on slap impact and export camera shake strength from agents.js. Update main.js to apply camera shake around the renderer.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b80e7d8c-b272-408c-8f8f-e4edd67ca534
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
Introduce `touchstart` event listener as a fallback for older browsers lacking Pointer Events, and reduce the interaction lockout timer from 220ms to 150ms to prevent accidental orbit drags after a slap.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b1d20c43-904b-495f-9262-401975d950d3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
Refactors the `buildTimmy` function to update Timmy's robe color to royal purple, add celestial gold star decorations, and implement a silver beard and hair, along with a pulsing orange magic orb effect.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7cc95df8-ef94-4761-8b47-9c13fedbba9a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
## What changed
- the-matrix/js/agents.js — face expression system added to Timmy wizard
## Face geometry (all parented to head — follow head.rotation.z tilt)
- White sclera eyes (MeshStandardMaterial f5f2e8, emissive 0x777777@0.10)
replace the old flat dark-blue spheres
- Dark pupils (MeshBasicMaterial 0x07070f) as child meshes of each sclera;
they scale with the parent eye for squint effect
- Mouth arc: TubeGeometry built from QuadraticBezierCurve3; control point
moves ±0.065 on Y for smile/frown; rebuilt via _buildMouthGeo() only when
|smileDelta| > 0.016 (throttled to avoid per-frame GC pressure)
- All face meshes are children of `head` — head.rotation.z carries every
face component naturally with the existing head-tilt animation
## FACE_TARGETS lookup table (lidScale, pupilScale, smileAmount)
- idle (contemplative): 0.44 / 0.90 / 0.08 — half-lid, neutral
- active (curious): 0.92 / 1.25 / 0.38 — wide eyes + dilated pupils, smile
- thinking (focused): 0.30 / 0.72 / -0.06 — squint + constricted pupils, flat
- working (attentive): 0.22 / 0.80 / 0.18 — very squint, slight grin
## setFaceEmotion(mood) exported API
- Accepts both task-spec names (contemplative|curious|focused|attentive)
and internal state names (idle|active|thinking|working) via MOOD_ALIASES
- Immediately sets faceTarget; lerp in updateAgents() handles the smooth transition
## Per-frame lerp (rate 0.055/frame) in updateAgents
- lidScale → eyeL.scale.y / eyeR.scale.y (squash for squint)
- pupilScale → pupilL.scale / pupilR.scale (uniform dilation)
- smileAmount → drives TubeGeometry rebuild when drift > 0.016
## Lip-sync while speaking (~1 Hz)
- speechTimer > 0: smileTarget = 0.28 + sin(t*6.283)*0.22
- Returns to mood target when timer expires
## Validation
- Vite build: clean (14 modules, 542 kB, no errors)
- testkit: 27/27 PASS (after server restart to clear rate-limit counters)
## What changed
- the-matrix/js/agents.js fully rewritten with face expression system
## Face geometry
- Replaced flat dark-blue eye spheres with white sclera (MeshStandardMaterial,
emissive 0x777777@0.10, roughness 0.55) + dark pupils (MeshBasicMaterial 0x07070f)
as child meshes of sclera
- Eyes are now children of the head mesh (not the group) so they naturally
follow head.rotation.z tilts driven by the existing animation loop
- Mouth added as a canvas Sprite (128x32, always faces camera) parented to the
group so it bobs with Timmy's body; drawn via quadraticCurveTo bezier arc
## Emotion → face parameter mapping (FACE_TARGETS table)
- idle (contemplative): lidScale=0.44, smileAmount=0.08 — half-lid, neutral
- active (curious): lidScale=0.92, smileAmount=0.38 — wide eyes, smile
- thinking (focused): lidScale=0.30, smileAmount=-0.06 — squint, flat mouth
- working (attentive): lidScale=0.22, smileAmount=0.18 — very squint, slight grin
## Per-frame lerp (updateAgents)
- faceParams lerped toward faceTarget at rate 0.055/frame (smooth, no snap)
- eyeL.scale.y / eyeR.scale.y driven by faceParams.lidScale (squash = squint)
- Mouth canvas redrawn only when |smileDelta| > 0.016 or speakingChanged
(avoids unnecessary texture uploads every frame)
## Lip-sync while speaking
- While speechTimer > 0: smileTarget = 0.28 + sin(t*6.283)*0.22 (~1 Hz)
- _drawMouth() renders two-lip "open mouth" shape when speaking=true
- Returns to mood expression when speechTimer expires
## Validation
- Vite build: clean (14 modules, 529 kB bundle, no errors)
- testkit: 27/27 PASS (no regressions)
- No out-of-scope changes (backend untouched)
## Task
Task #20: Timmy responds to Workshop input bar — make the "Say something
to Timmy…" input bar actually trigger an AI response shown in Timmy's
speech bubble.
## What was built
### Server (artifacts/api-server/src/lib/agent.ts)
- Added `chatReply(userText)` method to AgentService
- Uses claude-haiku (cheaper eval model) with a wizard persona system prompt
- 150-token limit so replies fit in the speech bubble
- Stub mode: returns one of 4 wizard-themed canned replies after 400ms delay
- Real mode: calls Anthropic with wizard persona, truncates to 250 chars
### Server (artifacts/api-server/src/routes/events.ts)
- Imported agentService
- Added per-visitor rate limit system: 3 replies/minute per visitorId (in-memory Map)
- Added broadcastToAll() helper for broadcasting to all WS clients
- Updated visitor_message handler:
1. Broadcasts visitor message to all watchers as before
2. Checks rate limit — if exceeded, sends polite "I need a moment…" reply
3. Fire-and-forget async AI call:
- Broadcasts agent_state: gamma=working (crystal ball pulses)
- Calls agentService.chatReply()
- Broadcasts agent_state: gamma=idle
- Broadcasts chat: agentId=timmy, text=reply to ALL clients
- Logs world event "visitor:reply"
### Frontend (the-matrix/js/websocket.js)
- Updated case 'chat' handler to differentiate message sources:
- agentId === 'timmy': speech bubble + event log entry "Timmy: <text>"
- agentId === 'visitor': event log only (don't hijack speech bubble)
- everything else (delta/alpha/beta payment notifications): speech bubble
## What was already working (no change needed)
- Enter key on input bar (ui.js already had keydown listener)
- Input clearing after send (already in ui.js)
- Speech bubble rendering (setSpeechBubble already existed in agents.js)
- WebSocket sendVisitorMessage already exported from websocket.js
## Tests
- 27/27 testkit PASS (no regressions)
- TypeScript: 0 errors
- Vite build: clean (the-matrix rebuilt)