## Summary

- **Fix emotion detection timeouts** by switching from batch-then-wait to continuous audio streaming — audio frames are now streamed to the Valence API as they arrive, and predictions are stored asynchronously in a timestamped ring buffer
- **Eliminate pipeline blocking** — when a FINAL_TRANSCRIPT arrives, emotion enrichment does an instant local lookup (~0.1ms) instead of sending audio and waiting for a response (previously 2s+ timeout)
- **Add performance logging** (`[PERF]` prefixed) for streaming health, prediction gaps, and enrichment timing

## Problem

The Valence API requires ~4.5 seconds of accumulated audio before producing an emotion prediction. The previous implementation buffered audio locally, sent it in a single batch when a FINAL_TRANSCRIPT arrived, then blocked waiting for the prediction with a 2-second timeout. Since the API needs ~5s of audio and the timeout was 2s, predictions almost always timed out, falling back to "neutral" for every transcript.

## Solution

### Continuous streaming (`client.py`)
- Added `start_streaming()` / `stop_streaming()` lifecycle methods
- Added `send_audio_chunk()` — fire-and-forget, sends each audio frame as it arrives so the server-side buffer accumulates audio in real time
- Predictions arrive asynchronously via the existing `on_prediction` Socket.IO handler and are stored in a timestamped ring buffer (max 20 entries, ~100s of predictions)
- Added `get_latest_emotion()` and `get_emotion_for_timerange(start_ms, end_ms)` for non-blocking lookups
- Legacy `process_audio()` batch method preserved with timeout increased from 2s to 7s

### Non-blocking enrichment (`stt.py`)
- `forward_audio()` now streams each frame to Valence via `asyncio.create_task()` (fire-and-forget) while simultaneously forwarding to the underlying STT
- Tracks audio position in milliseconds for timestamp correlation
- `_enrich_final_transcript()` uses `get_emotion_for_timerange()` to find the closest prediction — no network call, no blocking
- Removed `TimestampedFrame` dataclass and audio buffer (no longer needed)
- Removed `MAX_BUFFER_FRAMES` limit (no longer needed)

### Performance logging
- `[PERF] VALENCE | streaming_active` — logged every ~5s confirming audio is being streamed
- `[PERF] VALENCE | prediction_gap` — time between consecutive predictions
- `[PERF] VALENCE | first_prediction` — when the first prediction arrives
- `[PERF] EMOTION | enrichment` — time spent on enrichment (expected <1ms)
- `[PERF] EMOTION | text=...` — which emotion was selected and from which prediction timestamp

## Test plan

- [ ] Run the telephony agent with `DISABLE_VALENCE=false` and make a test call
- [ ] Confirm `[PERF] VALENCE | streaming_active` logs appear every ~5s
- [ ] Confirm `[PERF] VALENCE | prediction_gap` shows predictions arriving (~5-8s intervals)
- [ ] Confirm `[PERF] EMOTION | enrichment=<1ms` — validates non-blocking behavior
- [ ] Confirm transcripts are enriched with correct emotion tags (e.g., `[Sad] Hello?`)
- [ ] Verify no regression on the legacy `process_audio()` batch path
