---
phase: 20-vumeter-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - jam-ui/src/hooks/useVuHelpers.js
  - jam-ui/src/hooks/useMixerHelper.js
  # Note: VuContext.js not modified - it automatically exposes removeVuState via existing pattern
autonomous: true

must_haves:
  truths:
    - "VU meter state for a specific mixer can be removed when track leaves"
    - "vuStates object stays bounded as tracks join/leave the session"
    - "VU meters continue to work correctly after cleanup function is added"
  artifacts:
    - path: "jam-ui/src/hooks/useVuHelpers.js"
      provides: "removeVuState callback function"
      exports: ["removeVuState"]
    - path: "jam-ui/src/context/VuContext.js"
      provides: "VuContext with removeVuState exposed"
      contains: "removeVuState"
    - path: "jam-ui/src/hooks/useMixerHelper.js"
      provides: "Track leave cleanup integration"
      contains: "removeVuState"
  key_links:
    - from: "jam-ui/src/hooks/useVuHelpers.js"
      to: "jam-ui/src/context/VuContext.js"
      via: "export in useVuHelpers, re-export in VuContext"
      pattern: "removeVuState"
    - from: "jam-ui/src/hooks/useMixerHelper.js"
      to: "jam-ui/src/context/VuContext.js"
      via: "useVuContext import"
      pattern: "useVuContext.*removeVuState"
---

<objective>
Fix unbounded vuStates growth by adding a removeVuState cleanup function and integrating it with track lifecycle.

Purpose: Address HIGH priority memory leak (VUMTR-02, VUMTR-03) where vuStates object accumulates entries indefinitely as tracks join/leave sessions, contributing to session screen freeze after ~10 minutes.

Output:
- Modified useVuHelpers.js with removeVuState function
- Modified VuContext.js exposing removeVuState (automatic via existing pattern)
- Modified useMixerHelper.js calling removeVuState on mixer cleanup

Note: VUMTR-01 (VU callback throttling) is intentionally deferred as a performance optimization, not a memory leak (per Phase 19 audit).
</objective>

<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-audit-and-discovery/19-AUDIT.md

**Audit Finding (VUMTR-02 & VUMTR-03):**

The vuStates object at line 6 of useVuHelpers.js accumulates entries over time:

```javascript
const [vuStates, setVuStates] = useState({});  // Line 6 - never cleaned up per-mixer

const updateVuState = useCallback((mixerId, level, clipping = false) => {
  setVuStates(prev => ({
    ...prev,
    [mixerId]: { level, clipping }  // Adds new keys, never removes
  }));
}, []);  // Lines 99-104
```

**Problem:** When tracks are added/removed during a session, the vuStates object grows but entries for removed tracks are never deleted. Over a 10-minute session with multiple track changes, this accumulates hundreds of stale entries.

**Solution:** Add `removeVuState(mixerId)` function that deletes specific mixer entries and call it when tracks leave.
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add removeVuState function to useVuHelpers.js</name>
  <files>jam-ui/src/hooks/useVuHelpers.js</files>
  <action>
Add a new `removeVuState` callback function to useVuHelpers.js that removes a specific mixer's entry from the vuStates object.

Implementation:
1. After the existing `updateVuState` callback (around line 104), add:
   ```javascript
   const removeVuState = useCallback((mixerId) => {
     setVuStates(prev => {
       const newState = { ...prev };
       delete newState[mixerId];
       return newState;
     });
   }, []);
   ```

2. Add `removeVuState` to the return object (around line 174-184), alongside the existing exports:
   ```javascript
   return {
     // React-like components and functions
     VuMeter: VuMeterComponent,
     updateVuState,
     removeVuState,  // Add this line
     vuStates,
     // ... rest of exports
   };
   ```

The function uses the functional form of setVuStates to ensure it works correctly with React's state batching, and creates a shallow copy before deleting the key to maintain immutability.
  </action>
  <verify>
1. Read useVuHelpers.js to confirm removeVuState function exists
2. Verify it's included in the return statement
3. Run `npm run test:unit -- useVuHelpers` if test exists, or verify no syntax errors with `npm run lint` or a basic import check
  </verify>
  <done>
removeVuState callback exists in useVuHelpers.js and is exported in the return object
  </done>
</task>

<task type="checkpoint:pass">
  <name>Task 2: Verify removeVuState is exposed through VuContext</name>
  <rationale>
The VuContext.js file already passes all of useVuHelpers return values through the context provider (line 7: `const vuHelpers = useVuHelpers()`), so removeVuState is automatically available after Task 1 completes.

The existing pattern at lines 6-13 passes the entire vuHelpers object to the context:

```javascript
export const VuProvider = ({ children }) => {
  const vuHelpers = useVuHelpers();  // This now includes removeVuState

  return (
    <VuContext.Provider value={vuHelpers}>
      {children}
    </VuContext.Provider>
  );
};
```

No code changes required - consumers using `useVuContext()` will automatically have access to removeVuState once Task 1 adds it to useVuHelpers.
  </rationale>
  <verify>
1. Read VuContext.js to confirm the pattern passes all useVuHelpers exports
2. Verify useVuHelpers() is called and its result is passed to context value
3. Confirm no changes are needed to VuContext.js
  </verify>
  <done>
VuContext automatically exposes removeVuState through the existing vuHelpers spread pattern - no modifications needed
  </done>
</task>

<task type="auto">
  <name>Task 3: Integrate removeVuState with mixer lifecycle in useMixerHelper.js</name>
  <files>jam-ui/src/hooks/useMixerHelper.js</files>
  <action>
Add VU state cleanup when mixers are removed from the session.

Implementation steps:

1. **Import removeVuState from VuContext** - The file already imports `useVuContext` at line 61:
   ```javascript
   import { useVuContext } from '../context/VuContext.js';
   ```

   Modify line 111 from `const { updateVU3 } = useVuContext();` to `const { updateVU3, removeVuState } = useVuContext();` to destructure removeVuState alongside updateVU3.

2. **Add cleanup effect that watches for mixer removals** - After the existing `useEffect` that syncs `isReady` (around line 317), add a new effect that detects when mixers are removed and cleans up their VU state:

   ```javascript
   // Cleanup VU state when mixers are removed
   useEffect(() => {
     // Skip if not ready or no previous mixers to compare
     if (!isReadyRedux) return;

     // Get current mixer IDs from allMixers
     const currentMixerIds = new Set(Object.keys(allMixers));

     // Track previous mixer IDs using a ref
     if (!previousMixerIdsRef.current) {
       previousMixerIdsRef.current = currentMixerIds;
       return;
     }

     // Find removed mixers (in previous but not in current)
     for (const mixerId of previousMixerIdsRef.current) {
       if (!currentMixerIds.has(mixerId)) {
         // Mixer was removed - clean up its VU state
         removeVuState(mixerId);
         console.debug('[useMixerHelper] Cleaned up VU state for removed mixer:', mixerId);
       }
     }

     // Update previous for next comparison
     previousMixerIdsRef.current = currentMixerIds;
   }, [allMixers, isReadyRedux, removeVuState]);
   ```

3. **Add the ref for tracking previous mixer IDs** - Near the other refs (around line 67-68), add:
   ```javascript
   const previousMixerIdsRef = useRef(null);
   ```

This pattern:
- Compares current allMixers keys against previous keys on each update
- Calls removeVuState for any mixer that was removed
- Uses a ref to track previous state without causing re-renders
- Only runs when mixers are ready (isReadyRedux is true)
  </action>
  <verify>
1. Read useMixerHelper.js to confirm:
   - removeVuState is destructured from useVuContext
   - previousMixerIdsRef is defined
   - Cleanup useEffect exists and calls removeVuState
2. Run `npm run lint` to verify no syntax errors
3. Run existing tests: `npm run test:unit` to ensure no regressions
  </verify>
  <done>
- removeVuState is imported from VuContext
- previousMixerIdsRef tracks mixer IDs between renders
- useEffect detects removed mixers and calls removeVuState for cleanup
  </done>
</task>

</tasks>

<verification>
1. **Code verification:**
   - useVuHelpers.js exports removeVuState function
   - useMixerHelper.js imports and uses removeVuState
   - previousMixerIdsRef tracks mixer changes

2. **Lint check:**
   ```bash
   cd jam-ui && npm run lint -- --quiet
   ```

3. **Unit tests (if available):**
   ```bash
   cd jam-ui && npm run test:unit
   ```

4. **Build verification:**
   ```bash
   cd jam-ui && npm run build
   ```
</verification>

<success_criteria>
1. removeVuState function exists in useVuHelpers.js and is exported
2. useMixerHelper.js detects mixer removal and calls removeVuState
3. Code compiles without errors (npm run build passes)
4. Existing tests pass (no regressions)
5. Manual verification possible: Join session, check that vuStates doesn't grow unbounded as tracks join/leave
</success_criteria>

<output>
After completion, create `.planning/phases/20-vumeter-fixes/20-01-SUMMARY.md`
</output>
