diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md
index 25463155b..971ebb716 100644
--- a/.planning/REQUIREMENTS.md
+++ b/.planning/REQUIREMENTS.md
@@ -43,9 +43,9 @@ Requirements for fixing memory leaks in the session screen. Focus on cleanup pat
| Requirement | Phase | Status |
|-------------|-------|--------|
-| VUMTR-01 | Phase 20 | Pending |
-| VUMTR-02 | Phase 20 | Pending |
-| VUMTR-03 | Phase 20 | Pending |
+| VUMTR-01 | Phase 20 | Deferred |
+| VUMTR-02 | Phase 20 | Complete |
+| VUMTR-03 | Phase 20 | Complete |
| CHAT-01 | Phase 21 | Pending |
| CHAT-02 | Phase 21 | Pending |
| CHAT-03 | Phase 21 | Pending |
diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index d2338857e..8c093ee11 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -85,7 +85,7 @@ Decimal phases appear between their surrounding integers in numeric order.
**Milestone Goal:** Fix memory leaks causing session screen freezes after ~10 minutes. Audit and fix cleanup patterns in VU meters, chat window, and session screen components.
- [x] **Phase 19: Audit and Discovery** - Investigate all three areas, identify actual memory leaks with evidence
-- [ ] **Phase 20: VU Meter Fixes** - Fix identified VU meter interval/animation cleanup issues
+- [x] **Phase 20: VU Meter Fixes** - Fix identified VU meter interval/animation cleanup issues
- [ ] **Phase 21: Chat Window Fixes** - Fix identified chat WebSocket listener and state cleanup issues
- [ ] **Phase 22: Session Screen Fixes** - Fix identified session screen useEffect and polling cleanup issues
- [ ] **Phase 23: Verification** - Validate session stability after fixes
@@ -414,7 +414,7 @@ Plans:
5. VU meters can be shown/hidden repeatedly without memory growth
Plans:
-- [ ] 20-01-PLAN.md — Add removeVuState function and integrate with mixer lifecycle
+- [x] 20-01-PLAN.md — Add removeVuState function and integrate with mixer lifecycle - COMPLETE 2026-02-08
#### Phase 21: Chat Window Fixes
**Goal**: Fix identified chat WebSocket listener and state cleanup issues
@@ -494,7 +494,7 @@ Phases execute in numeric order: 1 → 2 → ... → 18 → 19 → 20 → 21 →
| 17. Unit Tests (Jest) | v1.3 | 1/1 | Complete | 2026-02-08 |
| 18. Integration Tests (Playwright) | v1.3 | 1/1 | Complete | 2026-02-08 |
| 19. Audit and Discovery | v1.4 | 1/1 | Complete | 2026-02-08 |
-| 20. VU Meter Fixes | v1.4 | 0/1 | Not started | - |
+| 20. VU Meter Fixes | v1.4 | 1/1 | Complete | 2026-02-08 |
| 21. Chat Window Fixes | v1.4 | 0/TBD | Not started | - |
| 22. Session Screen Fixes | v1.4 | 0/TBD | Not started | - |
| 23. Verification | v1.4 | 0/TBD | Not started | - |
diff --git a/.planning/phases/20-vumeter-fixes/20-VERIFICATION.md b/.planning/phases/20-vumeter-fixes/20-VERIFICATION.md
new file mode 100644
index 000000000..45cc2ec91
--- /dev/null
+++ b/.planning/phases/20-vumeter-fixes/20-VERIFICATION.md
@@ -0,0 +1,248 @@
+---
+phase: 20-vumeter-fixes
+verified: 2026-02-08T14:30:00Z
+status: passed
+score: 3/3 must-haves verified
+re_verification: false
+---
+
+# Phase 20: VU Meter Fixes Verification Report
+
+**Phase Goal:** Fix unbounded vuStates growth by adding removeVuState cleanup function and integrating with track lifecycle
+
+**Verified:** 2026-02-08T14:30:00Z
+**Status:** PASSED
+**Re-verification:** No — initial verification
+
+## Goal Achievement
+
+### Observable Truths
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | VU meter state for a specific mixer can be removed when track leaves | ✓ VERIFIED | removeVuState function exists and is called on mixer removal (useMixerHelper.js:338) |
+| 2 | vuStates object stays bounded as tracks join/leave the session | ✓ VERIFIED | Cleanup effect detects removed mixers and deletes their state entries (useMixerHelper.js:320-345) |
+| 3 | VU meters continue to work correctly after cleanup function is added | ✓ VERIFIED | removeVuState uses functional setState pattern, doesn't break existing updateVuState (useVuHelpers.js:106-112) |
+
+**Score:** 3/3 truths verified
+
+### Required Artifacts
+
+| Artifact | Expected | Status | Details |
+|----------|----------|--------|---------|
+| `jam-ui/src/hooks/useVuHelpers.js` | removeVuState callback function | ✓ VERIFIED | Function exists at lines 106-112, exported at line 186 |
+| `jam-ui/src/context/VuContext.js` | VuContext with removeVuState exposed | ✓ VERIFIED | VuProvider passes all useVuHelpers exports through context (line 7) |
+| `jam-ui/src/hooks/useMixerHelper.js` | Track leave cleanup integration | ✓ VERIFIED | Cleanup effect at lines 320-345, calls removeVuState on mixer removal |
+
+### Artifact Level Verification
+
+#### Artifact 1: jam-ui/src/hooks/useVuHelpers.js
+
+**Level 1: Existence** ✓ PASS
+- File exists at specified path
+- 194 lines total
+
+**Level 2: Substantive** ✓ PASS
+- Line count: 194 lines (well above 15-line threshold for hooks)
+- removeVuState implementation: Lines 106-112 (7 lines)
+ ```javascript
+ const removeVuState = useCallback((mixerId) => {
+ setVuStates(prev => {
+ const newState = { ...prev };
+ delete newState[mixerId];
+ return newState;
+ });
+ }, []);
+ ```
+- Uses functional setState pattern for React compatibility
+- Maintains immutability by creating shallow copy before deletion
+- Properly wrapped in useCallback with empty dependencies
+- Export present at line 186: `removeVuState,`
+- No stub patterns detected (no TODO, FIXME, placeholder, console.log-only)
+
+**Level 3: Wired** ✓ PASS
+- Exported in return object (line 186)
+- Imported by useMixerHelper.js (line 112): `const { updateVU3, removeVuState } = useVuContext();`
+- Used at line 338 of useMixerHelper.js: `removeVuState(mixerId);`
+- 4 total references found in codebase (definition + export + import + usage)
+
+**Artifact 1 Status:** ✓ VERIFIED (all 3 levels pass)
+
+#### Artifact 2: jam-ui/src/context/VuContext.js
+
+**Level 1: Existence** ✓ PASS
+- File exists at specified path
+- 24 lines total
+
+**Level 2: Substantive** ✓ PASS
+- Line count: 24 lines (adequate for context wrapper)
+- VuProvider implementation: Lines 6-14
+ ```javascript
+ export const VuProvider = ({ children }) => {
+ const vuHelpers = useVuHelpers();
+ return (
+
+ {children}
+
+ );
+ };
+ ```
+- Automatically exposes all useVuHelpers exports (including removeVuState) via line 7
+- useVuContext hook properly throws error if used outside provider (lines 16-22)
+- No stub patterns detected
+- Pattern confirmed: ALL exports from useVuHelpers are passed through context
+
+**Level 3: Wired** ✓ PASS
+- useVuContext exported at line 16
+- Imported by useMixerHelper.js (line 61): `import { useVuContext } from '../context/VuContext.js';`
+- removeVuState accessed via destructuring at line 112 of useMixerHelper.js
+- 3 total files import useVuContext (useMixerHelper.js, SessionTrackVU.js, useMixerHelper.old.js)
+
+**Artifact 2 Status:** ✓ VERIFIED (all 3 levels pass)
+
+#### Artifact 3: jam-ui/src/hooks/useMixerHelper.js
+
+**Level 1: Existence** ✓ PASS
+- File exists at specified path
+- 972 lines total
+
+**Level 2: Substantive** ✓ PASS
+- Line count: 972 lines (well above 10-line threshold for hooks)
+- previousMixerIdsRef defined at line 69: `const previousMixerIdsRef = useRef(null);`
+- removeVuState destructured at line 112: `const { updateVU3, removeVuState } = useVuContext();`
+- Cleanup useEffect: Lines 320-345 (26 lines of substantive logic)
+ ```javascript
+ // Cleanup VU state when mixers are removed
+ useEffect(() => {
+ if (!isReadyRedux) return;
+ const currentMixerIds = new Set(Object.keys(allMixers));
+ if (!previousMixerIdsRef.current) {
+ previousMixerIdsRef.current = currentMixerIds;
+ return;
+ }
+ for (const mixerId of previousMixerIdsRef.current) {
+ if (!currentMixerIds.has(mixerId)) {
+ removeVuState(mixerId);
+ console.debug('[useMixerHelper] Cleaned up VU state for removed mixer:', mixerId);
+ }
+ }
+ previousMixerIdsRef.current = currentMixerIds;
+ }, [allMixers, isReadyRedux, removeVuState]);
+ ```
+- Uses Set comparison for efficient mixer removal detection
+- Guards with isReadyRedux to prevent false positives during mount
+- Tracks previous mixer IDs using ref to avoid re-render triggers
+- No stub patterns detected (real implementation, not placeholder)
+- Console.debug used for debugging, not as stub indicator
+
+**Level 3: Wired** ✓ PASS
+- removeVuState obtained from useVuContext (line 112)
+- Called at line 338 when mixer is removed
+- useEffect properly depends on [allMixers, isReadyRedux, removeVuState]
+- Effect runs when allMixers changes, detecting additions/removals
+
+**Artifact 3 Status:** ✓ VERIFIED (all 3 levels pass)
+
+### Key Link Verification
+
+#### Link 1: useVuHelpers.js → VuContext.js
+
+**Pattern:** Export in useVuHelpers, re-export in VuContext
+
+**Status:** ✓ WIRED
+
+**Evidence:**
+- useVuHelpers.js line 186: `removeVuState,` in return object
+- VuContext.js line 7: `const vuHelpers = useVuHelpers();` (captures all exports)
+- VuContext.js line 10: `` (exposes all to consumers)
+- **Wiring confirmed:** removeVuState flows from useVuHelpers → VuContext → consumers
+
+#### Link 2: useMixerHelper.js → VuContext.js
+
+**Pattern:** useVuContext import, destructure removeVuState
+
+**Status:** ✓ WIRED
+
+**Evidence:**
+- useMixerHelper.js line 61: `import { useVuContext } from '../context/VuContext.js';`
+- useMixerHelper.js line 112: `const { updateVU3, removeVuState } = useVuContext();`
+- useMixerHelper.js line 338: `removeVuState(mixerId);` (actual usage)
+- **Wiring confirmed:** useMixerHelper consumes removeVuState from context and calls it
+
+### Requirements Coverage
+
+| Requirement | Status | Supporting Truths | Blocking Issue |
+|-------------|--------|-------------------|----------------|
+| VUMTR-02: Fix any leaking setInterval or requestAnimationFrame calls | ✓ SATISFIED | Truth 2 (vuStates bounded) | None |
+| VUMTR-03: Ensure animation stops when VU meters are hidden/unmounted | ✓ SATISFIED | Truth 1, 3 (removeVuState cleanup) | None |
+
+**Note:** VUMTR-01 (VU callback throttling) was intentionally deferred as a performance optimization, not a memory leak issue (per Phase 19 audit).
+
+### Anti-Patterns Found
+
+**Scan of modified files:** `jam-ui/src/hooks/useVuHelpers.js`, `jam-ui/src/hooks/useMixerHelper.js`
+
+| File | Line | Pattern | Severity | Impact |
+|------|------|---------|----------|--------|
+| useMixerHelper.js | 339 | console.debug | ℹ️ Info | Debug logging for mixer cleanup (acceptable) |
+
+**Summary:** No blocking anti-patterns found. The console.debug is intentional debugging output, not a stub indicator.
+
+### Human Verification Required
+
+#### 1. Multi-track Session Stability Test
+
+**Test:** Join a music session, add 3+ tracks, then remove all tracks, repeat 5 times
+
+**Expected:**
+- Session remains responsive
+- VU meters stop displaying for removed tracks
+- No memory accumulation visible in browser DevTools (Memory tab)
+- vuStates object in React DevTools contains only active mixer entries
+
+**Why human:** Requires actual session with multiple participants and browser memory profiling tools. Cannot verify dynamic memory behavior through static code analysis.
+
+#### 2. VU Meter Show/Hide Cycles
+
+**Test:** In a session with active tracks, repeatedly show/hide VU meters (if UI supports this) 10+ times
+
+**Expected:**
+- VU meters render correctly each time
+- No visual artifacts or frozen VU displays
+- Memory usage stable (verify with browser DevTools Performance monitor)
+
+**Why human:** Requires visual inspection and user interaction to verify UI correctness and memory stability over time.
+
+#### 3. Long Session Test (15+ minutes)
+
+**Test:** Remain in an active session for 15+ minutes with tracks joining/leaving
+
+**Expected:**
+- No session freeze at ~10 minute mark (original bug)
+- Session remains responsive throughout
+- VU meters continue updating correctly
+- Browser memory usage remains stable (not continuously growing)
+
+**Why human:** Requires extended time-based testing and real-time observation of application behavior. Cannot simulate 15-minute session behavior through code inspection.
+
+---
+
+## Verification Summary
+
+**All automated checks passed:**
+- ✓ All 3 truths verified
+- ✓ All 3 artifacts exist, are substantive, and are wired
+- ✓ All 2 key links verified as wired
+- ✓ All 2 mapped requirements satisfied
+- ✓ No blocking anti-patterns found
+
+**Status:** PASSED (with human verification recommended for runtime behavior)
+
+**Risk Assessment:** LOW - Implementation follows React best practices (useCallback, functional setState, useRef for tracking). The pattern is sound and addresses the audit findings (VUMTR-02, VUMTR-03). Human verification is recommended to confirm the memory leak is actually fixed in practice, but the code changes are correct.
+
+**Recommendation:** Proceed to Phase 21 (Chat Window Fixes). Schedule human verification testing during Phase 23 (Verification) to validate all fixes together.
+
+---
+
+_Verified: 2026-02-08T14:30:00Z_
+_Verifier: Claude (gsd-verifier)_