From Revert to Recovery: How Simplicity Won the Day
After reverting a “perfect” architectural fix, we shipped a working auto-recovery feature by embracing simplicity and invisible UX
Published: January 11, 2025
Tags: #SoloBuilder #AutoRecovery #Simplicity #UXDesign #ShippingWins #PragmaticEngineering
The “So What?” - Victory Through Simplification
Bottom Line Value: Learn how pivoting from a failed refactor to a simpler approach—combined with ruthless UX simplification—delivered a production-ready crash recovery feature in hours.
What You’ll Gain:
- How to recover momentum after a major revert
- The power of “always-on” vs. “configurable” features
- Why invisible UX is the best UX for safety features
- Real example: From broken architecture to shipped feature
- Framework for simplifying features that users shouldn’t think about
- ROI: Ship features faster by removing unnecessary complexity
Who This Helps: Solo builders recovering from failed refactors, engineers over-engineering features, product builders struggling with feature complexity.
Why Bother: The fastest path to shipping isn’t always the most elegant architecture. Sometimes the best feature is the one users never notice—until it saves their work.
TLDR: From Failure to Feature in One Day
Where we left off (Blog #130):
- ❌ Spent 6+ hours trying to eliminate dual audio path
- ❌ “Perfect” single-path architecture completely broke recording
- ✅ Made hard decision to revert everything
- ✅ Dual-path architecture restored, recording works perfectly
What happened next:
- ✅ Discovered checkpointing was never working (design flaw)
- ✅ Simplified: Made checkpointing always-on (removed all configuration)
- ✅ Tested: Force-quit via Activity Monitor (real crash simulation)
- ✅ Auto-recovery works perfectly end-to-end
- ✅ Feature shipped: Invisible, automatic, production-ready
The lesson: After a failed refactor, the path forward is often simpler than you think.
The Discovery: It Never Worked
The Moment of Truth
After reverting the dual-path refactor, we started testing the auto-recovery feature. The plan was simple:
- Start recording
- Speak for 90 seconds
- Check for checkpoint files
Expected result: Checkpoint files every 30 seconds
Actual result: No checkpoint files found
Wait, what?
The Root Cause: Optional by Default
The checkpoint system was designed to be “always-on” (per the design docs), but implemented as “optional”:
// The problem: Defaults to false
private var checkpointingEnabled: Bool = false
// Requires explicit configuration
func enableCheckpointing(sessionId: String, interval: TimeInterval) {
self.checkpointingEnabled = true
// ...
}
The TypeScript side:
// Requires passing parameters
checkpointing: {
enabled: true, // Could be false!
sessionId: generateSessionId(),
interval: 30
}
What went wrong:
- If parameters weren’t passed correctly → checkpointing disabled
- If there was a bug in parameter passing → checkpointing disabled
- Default state → checkpointing disabled
The feature was designed to always work, but implemented to usually fail.
The Pivot: Always-On Architecture
User’s Insight
“I want this feature always on. I’m not clear on why it needs to be initialized to false at all, or even parameterized. It should just be core to how we record.”
This changed everything.
The Simplification
Before (Optional):
// Swift side
private var checkpointingEnabled: Bool = false
func enableCheckpointing(sessionId: String, interval: TimeInterval)
func disableCheckpointing()
// Command-line parameters
--checkpoint-interval 30 --session-id 1234567890-123456
After (Always-On):
// Swift side
private let checkpointInterval: TimeInterval = 30.0 // Hardcoded
private var currentSessionId: String = "" // Auto-generated
// No command-line parameters needed
// No enable/disable methods
// Just works
What we removed:
- ❌
checkpointingEnabledflag - ❌
enableCheckpointing()method - ❌
disableCheckpointing()method - ❌ Command-line parameters
--checkpoint-intervaland--session-id - ❌ TypeScript parameter building code
- ❌ Session ID generation in TypeScript
- ❌ Entire
checkpointingconfiguration object
What we gained:
- ✅ Simpler code (less code = fewer bugs)
- ✅ Impossible to disable (can’t forget to enable it)
- ✅ No configuration needed (zero user-facing settings)
- ✅ Actually works (tested with real crash)
The UX Philosophy: Invisible Safety
The Design Constraint
From AUTO_RECOVERY_SUMMARY.md:
⚠️ CRITICAL UX CONSTRAINT: INVISIBLE TO NORMAL USERS
Auto-recovery must be COMPLETELY INVISIBLE unless a crash occurs.
This isn’t just a nice-to-have—it’s the entire point.
Why Invisible UX Matters
Bad UX (Visible Safety):
┌─────────────────────────────────────┐
│ ⚙️ Settings │
├─────────────────────────────────────┤
│ ☑ Enable crash recovery │
│ Checkpoint interval: [30] seconds │
│ Session ID format: [timestamp-rand] │
│ Cleanup policy: [After recovery] │
└─────────────────────────────────────┘
Problems:
- Users have to understand what “checkpointing” means
- Users might disable it (thinking it slows things down)
- Users have to configure intervals (what’s the right value?)
- More settings = more support questions
- Most importantly: Users shouldn’t have to think about crashes
Good UX (Invisible Safety):
┌─────────────────────────────────────┐
│ Recording... │
│ ⏺ 00:45 │
└─────────────────────────────────────┘
[Checkpoint files being written silently in background]
[User has no idea this is happening]
[User doesn't need to know]
If the app crashes:
┌─────────────────────────────────────┐
│ 🔄 Recovered Session │
├─────────────────────────────────────┤
│ We recovered your recording from │
│ before the crash (2m 30s). │
│ │
│ [View in History] [Dismiss] │
└─────────────────────────────────────┘
Benefits:
- ✅ User only sees recovery when it matters (after a crash)
- ✅ No configuration needed (zero cognitive load)
- ✅ No settings to explain (zero documentation needed)
- ✅ No way to disable it (zero support questions)
- ✅ Just works (zero user effort)
The Test: Real Crash Simulation
The Setup
Step 1: Start recording
- Press CTRL+SHIFT+` (manual recording mode)
- Speak continuously for 90+ seconds
Step 2: Verify checkpoint files
ls -lh /var/folders/.../T/recording_session-*_checkpoint-*.wav
Result:
-rw-r--r-- 1 user staff 1.1M Jan 11 14:23 recording_session-1762822825-196880_checkpoint-000.wav
-rw-r--r-- 1 user staff 1.1M Jan 11 14:23 recording_session-1762822825-196880_checkpoint-001.wav
-rw-r--r-- 1 user staff 1.1M Jan 11 14:24 recording_session-1762822825-196880_checkpoint-002.wav
✅ Checkpoint files being created at 30-second intervals
Step 3: Force-quit via Activity Monitor
- Open Activity Monitor
- Find “Electron” process
- Click Force Quit (X button)
- Confirm
This simulates a real crash (immediate termination, no cleanup)
Step 4: Restart VoiceCatcher
What happened:
- ✅ App detected checkpoint files on startup
- ✅ Automatically assembled audio from checkpoint files
- ✅ Automatically transcribed using Whisper
- ✅ Automatically saved to history
- ✅ Recovery notification appeared
- ✅ “View in History” button opened history window
- ✅ Recovered session visible with correct transcription
- ✅ Checkpoint files cleaned up after recovery
End-to-end auto-recovery: WORKING PERFECTLY 🎉
The Lessons: Simplicity Wins
1. Always-On > Configurable (For Safety Features)
When to make features always-on:
- Safety features (crash recovery, auto-save, backups)
- Features users shouldn’t think about
- Features that have no downside to being enabled
- Features where “forgetting to enable” = data loss
When to make features configurable:
- Features with performance trade-offs
- Features with different user preferences
- Features that change behavior users care about
- Features where users need control
Auto-recovery is clearly in the first category.
2. Invisible UX > Visible UX (For Background Features)
The best UX is no UX:
- Users don’t configure it
- Users don’t see it (until they need it)
- Users don’t think about it
- Users just trust it works
This is how macOS Time Machine works:
- No configuration needed (just enable once)
- Runs silently in background
- Only visible when you need to restore
- Users trust their data is safe
3. Simplification After Failure
After reverting the dual-path refactor, we could have:
- ❌ Tried another architectural approach
- ❌ Spent more time debugging the single-path solution
- ❌ Blamed the tools (Swift, AVFoundation, etc.)
Instead, we:
- ✅ Tested the existing feature
- ✅ Found it didn’t work (design flaw)
- ✅ Simplified the design (always-on)
- ✅ Shipped a working feature
The revert freed us to focus on what matters: shipping.
4. Test with Real Scenarios
We didn’t test with:
- ❌ Simulated crashes in code
- ❌ Graceful shutdown with cleanup
- ❌ Unit tests mocking the crash
We tested with:
- ✅ Force-quit via Activity Monitor
- ✅ Real checkpoint files in temp directory
- ✅ Real transcription with Whisper
- ✅ Real recovery notification
- ✅ Real user flow end-to-end
This is the only way to know it actually works.
The Framework: Simplify, Test, Ship
Step 1: Identify Unnecessary Complexity
Questions to ask:
- Does this need to be configurable?
- Does the user need to know about this?
- What happens if we remove this parameter?
- Can we hardcode this value?
- Is this feature optional or essential?
For auto-recovery:
- ❌ Checkpoint interval doesn’t need to be configurable (30s is fine)
- ❌ Session ID doesn’t need to be passed from TypeScript (generate in Swift)
- ❌ Enable/disable doesn’t make sense (always enable)
- ✅ Remove all of it
Step 2: Make It Always-On
Code changes:
- private var checkpointingEnabled: Bool = false
+ private let checkpointInterval: TimeInterval = 30.0
- func enableCheckpointing(sessionId: String, interval: TimeInterval) {
- self.checkpointingEnabled = true
- self.currentSessionId = sessionId
- self.checkpointInterval = interval
- }
+ // Checkpointing is always enabled, no method needed
- if checkpointingEnabled {
- saveCheckpoint()
- }
+ saveCheckpoint() // Always save
Result: Feature can’t be disabled, can’t be misconfigured, can’t fail to initialize.
Step 3: Test with Real Crashes
Don’t simulate—actually crash:
- Start the feature
- Force-quit the app (Activity Monitor,
kill -9, etc.) - Restart the app
- Verify recovery works end-to-end
If it doesn’t work, you’ll know immediately.
Step 4: Ship It
Once it works:
- ✅ Document the feature (user guide)
- ✅ Update help system
- ✅ Write blog post (you’re reading it!)
- ✅ Ship to users
Don’t wait for perfection—ship working features.
The Metrics: What We Shipped
Code Removed (Simplification)
Swift:
- Removed
checkpointingEnabledflag - Removed
enableCheckpointing()method - Removed
disableCheckpointing()method - Removed command-line parameter parsing for checkpoints
- ~50 lines of code deleted
TypeScript:
- Removed
checkpointingparameter fromRecordAudioOptions - Removed checkpoint parameter building in
buildArgs() - Removed
generateSessionId()fromrecording-controller.ts - Removed checkpoint config from
recordAudio()calls - ~40 lines of code deleted
Total: ~90 lines of code removed ✂️
Features Shipped (Value)
Auto-Recovery:
- ✅ Checkpoint files created every 30 seconds during recording
- ✅ Checkpoint files survive crashes (stored in system temp)
- ✅ Automatic detection on app restart
- ✅ Automatic audio assembly (GPL-free, pure Node.js)
- ✅ Automatic transcription (Whisper.cpp)
- ✅ Automatic save to history
- ✅ Recovery notification with “View in History” button
- ✅ Checkpoint cleanup after successful recovery
- ✅ Completely invisible to users (unless crash occurs)
User Impact:
- 🎯 Zero data loss from crashes
- 🎯 Zero configuration needed
- 🎯 Zero cognitive load (users don’t think about it)
- 🎯 100% automatic (no user action required)
The Takeaway: Pivot, Simplify, Ship
What We Learned
From the revert (Blog #130):
- Know when to abandon a failing approach
- Reverting is not failure—it’s pragmatism
- Working code > elegant code
From the recovery (this post):
- Simplify ruthlessly (always-on > configurable)
- Invisible UX is the best UX (for safety features)
- Test with real scenarios (force-quit, not mocks)
- Ship working features (don’t wait for perfect)
The Pattern
Failed Refactor
↓
Revert to Working State
↓
Test Existing Features
↓
Find Design Flaws
↓
Simplify Ruthlessly
↓
Test with Real Scenarios
↓
Ship Working Feature
This is how solo builders ship: Pivot fast, simplify hard, test real, ship working.
Next Steps
What’s shipping next:
- 📝 Update user documentation (recovery feature)
- 📝 Update help system (crash recovery section)
- 🧪 Add automated tests (checkpoint creation, recovery flow)
- 🚀 Ship v0.4.1 with auto-recovery
What we’re NOT doing:
- ❌ Trying to eliminate dual audio path again
- ❌ Adding checkpoint configuration settings
- ❌ Making recovery “smarter” (it works, ship it)
The focus: Ship features that work, not features that are perfect.
Resources
- Blog #130: Know When to Fold ‘Em - The revert that made this possible
- AUTO_RECOVERY_SUMMARY.md - Original design docs
- VoiceCatcher Repository - See the code
The Bottom Line: After reverting a failed refactor, we shipped a production-ready auto-recovery feature by making it always-on, invisible, and simple. Sometimes the best path forward is the one with the least code.
Ship working features. Simplify ruthlessly. Make UX invisible.
Written by a solo builder who learned that the best code is often the code you delete.