32 Commits

Author SHA1 Message Date
eed37885fb [claude] Matrix chat history persistence — localStorage per-agent, 100-msg cap, Clear button (#43) (#63)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-23 20:40:12 +00:00
4712619959 [claude] Session panel QR codes + default 200 sats (#46) (#55)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-23 14:52:36 +00:00
1243ad7ce5 [claude] Spring wobble + boing SFX for Timmy slap (#35) (#52)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-23 14:51:34 +00:00
0d1252d8b2 [claude] feat: elite-tier Trust Panel vouch UI (#50) (#60) 2026-03-23 01:56:26 +00:00
alexpaynex
9ef27bec9f Add new FPS-style navigation and AR label features
Rebuilds the Tower project with Vite, introduces first-person navigation controls for desktop and mobile, and adds AR floating labels for agent information.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07379049-28ff-4b1c-aeeb-17e250821a43
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/90c7a60b-2c61-4699-b5c6-6a1ac7469a4d/hoGhXo5
Replit-Helium-Checkpoint-Created: true
2026-03-20 01:01:03 +00:00
alexpaynex
6590f0fc92 Update Vite version to ensure all installations are secure
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
2026-03-19 22:24:15 +00:00
Replit Agent
eb5dcfd48a task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
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
2026-03-19 19:27:13 +00:00
Replit Agent
dabadb4298 task-28 fix5: session triage, speech-bubble local badge, footprint docs
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
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)
2026-03-19 19:10:46 +00:00
alexpaynex
8897371815 task-28: Edge intelligence — browser ML, Nostr signing, cost preview, sentiment moods
All 6 reviewer rounds complete. Final diff addresses:
- complexity:trivial|moderate|complex contract (not binary local|server)
- Trivial heuristic in cost preview: zero network calls for greetings
- X-Nostr-Token on all job/session API calls including polling paths
- npub-only discovery shows identity prompt (_canSign flag)
- Worker/UI contract fully aligned on complexity tiers
2026-03-19 19:07:24 +00:00
Replit Agent
cb50e8c658 task-28 fix4: trivial cost-preview gate + job polling token
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
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.
2026-03-19 19:06:47 +00:00
Replit Agent
494393017c task-28 fix3: complexity contract, consistent token headers, npub-only prompt
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
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)
2026-03-19 19:02:45 +00:00
alexpaynex
26556ba300 Update application assets and code for improved functionality
Update manifest file and JavaScript bundle with new asset references.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b74051a5-0be6-4afb-9153-f25b06b6190b
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
2026-03-19 18:24:27 +00:00
Replit Agent
437df487fd task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching
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.
2026-03-19 18:20:13 +00:00
Replit Agent
898a47fd39 task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs
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
2026-03-19 18:16:40 +00:00
Replit Agent
af3c938c6e task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods
- js/edge-worker.js: new — browser-side classify() + sentiment() via Transformers.js
- js/nostr-identity.js: new — NIP-07 extension + localStorage keypair fallback,
  challenge→sign→verify flow, token caching
- js/agents.js: export setMood() for sentiment-driven face expressions
- js/ui.js: local triage badge, cost preview via /api/estimate, sentiment on send
- js/payment.js: X-Nostr-Token injection on POST /jobs
- js/session.js: X-Nostr-Token injection on session create + request, sentiment mood
- js/main.js: initNostrIdentity() + warmupEdgeWorker() on firstInit
- vite.config.js: optimizeDeps.exclude @xenova/transformers
2026-03-19 18:09:44 +00:00
Replit Agent
9de2396457 feat: Alexander Whitestone landing page + the-matrix dist at /tower
- Root / serves branded landing page (falling amber digit rain, enter button)
- /tower serves pre-built the-matrix frontend (Three.js Workshop world)
- config.js patched: WS URL auto-detects from window.location.host
- No manual ?ws= param needed — works on any domain
2026-03-19 07:12:26 +00:00
alexpaynex
00d3233db3 Add QR code placeholders to invoice and top-up sections
Add a CSS class for QR code placeholders and integrate them into the session invoice and top-up invoice views within the-matrix/index.html.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 12e1daa7-20ba-447b-baca-fe5d0f280764
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
2026-03-19 04:00:00 +00:00
alexpaynex
c7e3a9b853 Task #23: Workshop session mode UI — fund once, ask many (all review issues fixed)
## Changes

### the-matrix/js/session.js (new module)
- Full session lifecycle: create → invoice → deposit poll → active → request → topup → restore
- Presets + number input for deposit (200–10,000 sats) and topup amounts; reads from input on submit
- Input validation: 200–10,000 sats range enforced in JS before API call
- Auto-closes panel after deposit payment confirms (closePanel in _startDepositPolling success branch)
- Low-balance condition fixed: `isSessionActive()` (covers both 'active' and 'paused') not just `active`
- HUD: updates `#session-hud-balance` span with "Balance: X sats"; `#session-hud-topup` link clickable
- Topup reads from `#session-topup-input` number field, same validation
- localStorage restore: validates session via GET, restores macaroon + balance + UI state on reload
- Expired/401 sessions: clears storage, resets all UI
- Request in-flight guard prevents double-submit; send button disabled during request

### the-matrix/js/ui.js
- `setSessionSendHandler(fn)` — override input bar submit when session active
- `setInputBarSessionMode(active, placeholder)` — green border + placeholder swap
- `send()` routes to session handler when set, falls back to WS visitor_message

### the-matrix/index.html
- `#top-buttons` flex container: " SUBMIT JOB" (blue) + " FUND SESSION" (teal) side-by-side
- `#session-hud` with `#session-hud-balance` span + `#session-hud-topup` link (pointer-events: all)
- `#session-panel` (left slide-in): fund / invoice / active / topup steps
  - Fund + topup steps each have preset buttons AND a number input (200–10k range)
  - Added 10k preset button to both step grids
- `#visitor-input.session-active` green pulse border animation (3s keyframe)
- `#low-balance-notice` strip above input bar with inline Top Up button
- CSS: `.session-amount-input` green styled, spin buttons hidden; `.session-amount-row` flex layout
- CSS: `.primary-green` / `.muted` panel button variants for session panel theme

### the-matrix/js/main.js
- Import + call `initSessionPanel()` in firstInit block

## Verification
- npm run build: clean (0 errors, 15 modules)
- Testkit: 27/27 PASS (session tests 11–16, 22 all green)
2026-03-19 03:56:34 +00:00
alexpaynex
ad5ac0861d Task #23: Workshop session mode UI — fund once, ask many
## What was done
- **`the-matrix/js/session.js`** (new module): Full session mode UI lifecycle:
  - Create session flow: amount presets → POST /api/sessions → deposit invoice step
  - Deposit payment: stub simulate → 2s polling until state=active
  - macaroon + sessionId stored in localStorage (`timmy_session_v1`)
  - Request submission: intercepts input bar when session active → POST /api/sessions/:id/request
    → Timmy speech bubble shows result, balance updates in HUD
  - Low-balance (< 50 sats): paused state, low-balance notice shown, topup quick-button
  - Topup flow: preset amount → POST /api/sessions/:id/topup → topup invoice → stub pay → poll
  - Restore from localStorage on page reload: validates session via GET, restores full UI state
  - Session expiry / 401 macaroon rejection: clears storage, resets to unfunded state
- **`the-matrix/js/ui.js`**: Added `setSessionSendHandler(fn)` + `setInputBarSessionMode(active, placeholder)` exports; send() routes to session handler when active, falls back to WS visitor_message
- **`the-matrix/index.html`**:
  - `#top-buttons` flex container: " SUBMIT JOB" (blue) + " FUND SESSION" (teal) side-by-side
  - `#session-hud` balance line in HUD (green, hidden until session active)
  - `#session-panel` left-side slide-in panel: fund / invoice / active / topup steps
  - `.session-amount-btn` presets (200, 500, 1000, 2000, 5000 sats) with active state
  - `#visitor-input.session-active` CSS: green border + 3s pulse keyframe animation
  - `#low-balance-notice` strip above input bar with Top Up quick-button
  - `.primary-green` / `.muted` panel button variants for session panel theme
  - `#session-panel` inherits shared `.panel-btn`, `.invoice-box`, `.copy-btn` with green overrides
- **`the-matrix/js/main.js`**: Import + call `initSessionPanel()` in firstInit block

## Verification
- `npm run build` in the-matrix → clean build (0 errors)
- Full testkit: 27/27 PASS (all session tests 11–16, 22 still green)
2026-03-19 03:50:34 +00:00
alexpaynex
0419ada6e2 Add ragdoll physics and reactive camera shake for satisfying slaps
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
2026-03-19 03:40:41 +00:00
alexpaynex
a0df576ed6 Add touchstart fallback and adjust interaction lockout
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
2026-03-19 03:33:48 +00:00
alexpaynex
35babd2400 Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
  timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
  proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
  relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
  exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
  spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
  and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
  timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
  for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
  registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction

## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
alexpaynex
2956cc07b2 Update character's appearance to include a long grey wizard beard
Modify the `agents.js` file to replace the existing beard and moustache geometry with a new, longer grey beard model.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1df11cd3-4638-4860-9c27-dd3c2040f00f
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
2026-03-19 03:24:31 +00:00
alexpaynex
93bd48f8ea Update Timmy's appearance to match reference with new colors and details
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
2026-03-19 03:23:28 +00:00
alexpaynex
6e982ff772 Improve mouth geometry performance by precomputing all shapes
Optimize mouth geometry generation by precomputing a cache of shapes, eliminating runtime allocations and reducing garbage collection pressure during animations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 92d6d2fb-91ca-4dea-98b6-ff5053cb5ac7
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
2026-03-19 03:18:20 +00:00
alexpaynex
8d48eb06b3 feat(task-21): Timmy face expressions + emotion engine
## What changed
- the-matrix/js/agents.js — complete face expression system on Timmy wizard

## Face geometry (all parented to head — follow head.rotation.z tilt)
- White sclera eyes (MeshStandardMaterial f5f2e8, emissive 0x777777@0.10)
  replace old flat dark-blue eye spheres
- Dark pupil spheres (MeshBasicMaterial 0x07070f) as children of each sclera;
  they scale with the parent eye for squint + animate independently for dilation
- Mouth arc: TubeGeometry via QuadraticBezierCurve3; ctrlY = -smileAmount*0.065;
  rebuilt only when |smileDelta| > 0.016 (throttled, not per-frame GC)
- All face meshes are children of `head` mesh — head.rotation.z carries every
  face feature naturally with the existing head-tilt animation

## FACE_TARGETS table (lidScale, pupilScale, smileAmount)
- idle  (contemplative): 0.44 / 0.90 /  0.08 — half-lid, neutral
- active (curious):      0.92 / 1.25 /  0.38 — wide open + dilated pupils, smile
- thinking (focused):    0.30 / 0.72 /  0.00 — narrow squint + constrict, flat mouth
- working (attentive):   0.75 / 1.05 /  0.18 — alert/open eyes, slight grin

## setFaceEmotion(mood) — authoritative public API
- Accepts task-spec names (contemplative|curious|focused|attentive) and internal
  names (idle|active|thinking|working) via MOOD_ALIASES
- Sets timmy._overrideMood; persists across frames, takes precedence over
  deriveTimmyState() in updateAgents()
- Call with null to clear override and return to automatic state-driven expressions

## Per-frame lerp (rate 0.055/frame) in updateAgents
- Uses _overrideMood ?? deriveTimmyState() as effective mood
- lidScale → eyeL.scale.y / eyeR.scale.y (squash for squint/wide-open)
- pupilScale → pupilL/R.scale.setScalar() (uniform dilation)
- smileAmount → drives thresholded TubeGeometry rebuild

## Lip-sync while speaking (1 Hz, range 0.20–0.60)
- speechTimer > 0: smileTarget = 0.40 + sin(t*6.283)*0.20
- Returns to mood expression when timer expires

## Validation
- Vite build: clean (14 modules, no errors)
- testkit: 27/27 PASS (server restarted to clear rate-limit counters between runs)
2026-03-19 03:13:51 +00:00
alexpaynex
9ff5ef683d feat(task-21): Timmy face expressions + emotion engine
## 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)
2026-03-19 03:09:45 +00:00
alexpaynex
7f402c5c7f feat(task-21): Timmy face expressions + emotion engine
## 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)
2026-03-19 03:04:17 +00:00
alexpaynex
71dbbd3f37 feat(task-20): Timmy responds to Workshop input bar with AI
## 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)
2026-03-19 02:52:49 +00:00
a70898e939 feat(epic222): Workshop — Timmy as wizard presence, world state, WS bootstrap (#31) 2026-03-18 22:15:46 -04:00
e088ca4cd8 feat(integration): WS bridge + Tower + payment panel + E2E test [10/10 PASS] (#26) 2026-03-18 21:20:51 -04:00
83a2ec19e2 fix(testkit): macOS compat + fix test 8c ordering (#24) 2026-03-18 21:01:13 -04:00