Fixes#200
## Problem
When executeCompact() recovery fails unexpectedly or gets interrupted,
the compactionInProgress lock is never cleared, permanently blocking both
auto-compact AND manual /compact for the session.
## Root Cause
- No try/finally around lock acquisition (line 261)
- Silent blocking when lock held - no user feedback
- Lock cleanup scattered across 7 manual deletion points
- Any unexpected exception bypasses cleanup, leaving lock stuck forever
## Solution
1. **Try/Finally Lock Guarantee**: Wrapped entire executeCompact body in
try/finally block to guarantee lock cleanup, following the pattern
used in preemptive-compaction hook
2. **User Feedback**: Added toast notification when compact attempt is
blocked by existing lock, replacing silent failure with clear warning
3. **Removed Redundancy**: Removed 6 redundant manual lock deletions
(kept only clearSessionState and finally block)
## Testing Evidence
✅ 10/10 comprehensive tests pass
✅ Lock cleared on successful completion
✅ Lock cleared when summarize throws
✅ Lock cleared when revert throws
✅ Lock cleared when fixEmptyMessages executes
✅ Lock cleared when truncation is sufficient
✅ Lock cleared after max recovery attempts
✅ Lock cleared when toast fails
✅ Lock cleared when prompt_async throws
✅ Toast shown when lock already held
✅ TypeScript type check passes with zero errors
## Files Changed
- executor.ts: Added try/finally, toast notification, removed 6 redundant deletions
- executor.test.ts: New comprehensive test suite (10 tests, 13 assertions)
## Impact
- Severity: High → Fixed
- User Experience: No more stuck sessions requiring restart
- Behavior: Identical except lock now guaranteed to clear
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>