Adjust the minimum Vite version in package.json to a patched release to prevent future installations of vulnerable versions.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90273644-97c2-4c11-b04c-7c482fb655b7
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1c2c17ee-1560-426f-89f7-07e87e9acd1a
Replit-Helium-Checkpoint-Created: true
1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts)
- Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode)
- Generates and warns about ephemeral key if env var absent
- sign(EventTemplate) → finalizeEvent() with Timmy's key
- encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt()
- Logs npub at server startup
2. ZapService (artifacts/api-server/src/lib/zap.ts)
- Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key
- Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware)
- Logs every outbound event to timmy_nostr_events audit table
- maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess()
- Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10)
- Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0
3. Engagement engine (artifacts/api-server/src/lib/engagement.ts)
- Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled)
- Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold
- Generates personalised DM via agentService.chatReply()
- Encrypts as NIP-04 DM (kind 4), signs with Timmy's key
- Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set
- First run delayed 60s after startup to avoid cold-start noise
4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts)
- POST /api/identity/vouch: requires X-Nostr-Token with elite tier
- Verifies optional Nostr event signature from voucher
- Records relationship in nostr_trust_vouches table
- Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score
- GET /api/identity/timmy: public endpoint returning npub + zap count
5. DB schema additions (lib/db/src/schema/)
- timmy_nostr_events: audit log for all outbound Nostr events
- nostr_trust_vouches: voucher/vouchee social graph with boost amount
- Tables created in production DB via drizzle-kit push
6. Frontend identity card (the-matrix/)
- #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count
- timmy-id.js: initTimmyId() fetches /api/identity/timmy on load
- Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback
- Refreshes every 60s to show live zap count
- Wired into main.js on firstInit
1. ui.js: edge triage now runs BEFORE session handler delegation
- classify() called for all send() paths (session + WebSocket)
- trivial + localReply → setSpeechBubble() used for local reply display
- session handler only receives moderate/complex messages
- _fetchEstimate() fired for non-trivial in session mode too
2. edge-worker.js: quantization footprint documented (~87MB int8, cached)
1. ui.js: _scheduleCostPreview() now gates on _TRIVIAL_RE before scheduling
the /api/estimate fetch. Greeting-pattern text shows '0 sats' badge locally
and never makes a network call. Same regex as edge-worker.js _isGreeting().
2. payment.js: startPolling() GET /api/jobs/:id now attaches X-Nostr-Token
header on every poll cycle via getOrRefreshToken(). Completes consistent
X-Nostr-Token coverage across all job/session API calls.
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)
1. nostr-identity.js: _scanExistingNostrKeys() discovers pre-existing Nostr keys
in localStorage using common patterns: nsec1/npub1 bech32, raw hex privkey,
JSON objects with nsec/npub/privkey fields. Scans common client key names
(nostr_privkey, privkey, nsec, nostr-nsec, nostrKeys, etc.) before showing
the identity prompt. Keys discovered are re-saved in app format for next load.
2. ui.js: _fetchEstimate() now sends nostr_token as X-Nostr-Token header instead
of query param, consistent with all other authenticated API calls.
3. edge-worker.js: explicit env.useBrowserCache=true + env.allowLocalModels=false
so model weights are cached in browser Cache API after first download.
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)