Files
alfred-mobile/RECONNECTION.md
jknapp 6d4ae2e5c3 Initial commit: Alfred Mobile - AI Assistant Android App
- OAuth authentication via Authentik
- WebSocket connection to OpenClaw gateway
- Configurable gateway URL with first-run setup
- User preferences sync across devices
- Multi-user support with custom assistant names
- ElevenLabs TTS integration (local + remote)
- FCM push notifications for alarms
- Voice input via Google Speech API
- No hardcoded secrets or internal IPs in tracked files
2026-02-09 11:12:51 -08:00

9.8 KiB

Auto-Reconnection Feature

Alfred mobile app now automatically reconnects when the connection is lost!

Overview

The app now includes intelligent auto-reconnection with exponential backoff to handle network issues gracefully. When the WebSocket connection drops, the app will automatically attempt to reconnect without requiring user intervention.

Features

Automatic Reconnection

  • Triggered on any connection failure:
    • Network interruptions
    • Server disconnections
    • "Software caused connection abort" errors
    • WiFi/cellular switching
    • Proxy restarts

📈 Exponential Backoff

  • Smart retry timing:
    • Attempt 1: 1 second delay
    • Attempt 2: 2 seconds delay
    • Attempt 3: 4 seconds delay
    • Attempt 4: 8 seconds delay
    • Attempt 5: 16 seconds delay
    • Attempts 6+: 30 seconds delay (max)

🔢 Max Retry Limit

  • 10 reconnection attempts before giving up
  • Prevents infinite retry loops
  • Battery-friendly

📊 UI Feedback

  • Connection status shows:
    • "Connecting..." - Initial connection
    • "Connected " - Successfully connected
    • "Disconnected" - Connection lost
    • "Reconnecting... (attempt X, Ys)" - Auto-reconnecting
    • "Error: Connection lost - max retries exceeded" - Gave up after 10 attempts

How It Works

Connection Loss Detected
         │
         ▼
┌────────────────────┐
│  shouldReconnect?  │
│     (enabled)      │
└────────┬───────────┘
         │ yes
         ▼
┌────────────────────┐
│  Check attempt #   │
│   < 10 attempts?   │
└────────┬───────────┘
         │ yes
         ▼
┌────────────────────┐
│ Calculate delay    │
│ (exponential)      │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│  Schedule retry    │
│   (Handler)        │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│  Wait delay...     │
└────────┬───────────┘
         │
         ▼
┌────────────────────┐
│ Attempt connect    │
└────────┬───────────┘
         │
         ├─ Success → Reset counter, connected ✅
         │
         └─ Failure → Loop back to retry

Code Details

GatewayClient.kt Changes

New State Variables:

private var shouldReconnect = true
private var reconnectAttempts = 0
private val maxReconnectAttempts = 10
private val baseReconnectDelayMs = 1000L
private val maxReconnectDelayMs = 30000L
private var reconnectHandler: Handler? = null

Reconnection Logic:

private fun scheduleReconnect() {
    if (reconnectAttempts >= maxReconnectAttempts) {
        Log.e(TAG, "Max reconnection attempts reached")
        listener.onError("Connection lost - max retries exceeded")
        shouldReconnect = false
        return
    }
    
    val delay = minOf(
        baseReconnectDelayMs * (1 shl reconnectAttempts),
        maxReconnectDelayMs
    )
    
    reconnectAttempts++
    listener.onReconnecting(reconnectAttempts, delay)
    
    reconnectHandler?.postDelayed({
        if (shouldReconnect && !isConnected) {
            connect()
        }
    }, delay)
}

Reset on Success:

private fun resetReconnectionState() {
    reconnectAttempts = 0
    reconnectHandler?.removeCallbacksAndMessages(null)
    reconnectHandler = null
}

GatewayListener Interface

New Callback:

interface GatewayListener {
    // ... existing callbacks ...
    fun onReconnecting(attempt: Int, delayMs: Long)
}

MainScreen.kt Implementation

override fun onReconnecting(attempt: Int, delayMs: Long) {
    val delaySec = delayMs / 1000
    connectionStatus = "Reconnecting... (attempt $attempt, ${delaySec}s)"
    Log.d("MainScreen", "Reconnecting: attempt $attempt, delay ${delayMs}ms")
}

Testing

Test Scenarios

1. Restart OAuth Proxy

# Kill and restart the proxy
pkill -f "node server.js"
cd ~/.openclaw/workspace/alfred-proxy
node server.js > /tmp/alfred-proxy.log 2>&1 &

Expected behavior:

  • App shows "Disconnected"
  • After ~1 second: "Reconnecting... (attempt 1, 1s)"
  • After ~3 seconds: "Connected "

2. Network Switch (WiFi ↔ Cellular)

  • Turn off WiFi on mobile device
  • App automatically reconnects via cellular
  • Or vice versa

3. Temporary Network Loss

  • Enable airplane mode for 5 seconds
  • Disable airplane mode
  • App reconnects automatically

4. Extended Network Outage

  • Enable airplane mode for 5 minutes
  • App will attempt 10 reconnections over ~2 minutes
  • Shows "Connection lost - max retries exceeded"
  • Disable airplane mode
  • Restart app to reconnect

Manual Testing

  1. Connect the app

    • Status should show "Connected "
  2. Kill the proxy

    pkill -f "node server.js"
    
    • Watch the status bar for reconnection attempts
  3. Restart the proxy

    cd ~/.openclaw/workspace/alfred-proxy
    node server.js > /tmp/alfred-proxy.log 2>&1 &
    
    • App should reconnect within a few seconds
  4. Check logs

    adb logcat GatewayClient:D MainScreen:D *:S
    
    • Should see reconnection attempts and backoff delays

Edge Cases

When Reconnection is Disabled

User-initiated disconnect:

  • Logging out
  • App closure
  • Manual disconnect

The shouldReconnect flag is set to false in these cases to prevent unwanted reconnection attempts.

Max Retries Reached

After 10 failed attempts:

  • shouldReconnect is set to false
  • Error message displayed
  • User must restart the app to reconnect

Why 10 attempts?

  • Total time: ~2 minutes of reconnection attempts
  • Balance between persistence and battery life
  • Prevents infinite loops

State Cleanup

On successful connection:

  • reconnectAttempts reset to 0
  • Reconnection handler cleared
  • Fresh state for next potential disconnection

On manual disconnect:

  • shouldReconnect set to false
  • All handlers cancelled
  • Clean shutdown

Benefits

User Experience

  • Seamless reconnection after temporary network issues
  • No manual intervention required
  • Clear status feedback
  • Works across network type changes (WiFi ↔ Cellular)

Battery Efficiency

  • Exponential backoff reduces connection attempts over time
  • Max retry limit prevents infinite loops
  • Handler cleanup prevents memory leaks

Reliability

  • Handles "Software caused connection abort" errors
  • Resilient to proxy/server restarts
  • Graceful degradation (max retries)

Monitoring

Logs to Watch

GatewayClient:

D/GatewayClient: WebSocket failure
D/GatewayClient: Scheduling reconnect attempt 1 in 1000ms
D/GatewayClient: Attempting reconnection (attempt 1)
D/GatewayClient: Connect successful!

MainScreen:

D/MainScreen: Reconnecting: attempt 1, delay 1000ms
D/MainScreen: Reconnecting: attempt 2, delay 2000ms

Status Bar Messages

Status Meaning
Connecting... Initial connection in progress
Connected Successfully connected
Disconnected Connection lost
Reconnecting... (attempt 1, 1s) First reconnection attempt
Reconnecting... (attempt 5, 16s) Fifth attempt with backoff
Error: Connection lost - max retries exceeded Gave up after 10 attempts

Configuration

Adjustable Parameters

Edit GatewayClient.kt to customize:

// Maximum reconnection attempts
private val maxReconnectAttempts = 10  // Change to 5, 20, etc.

// Initial retry delay
private val baseReconnectDelayMs = 1000L  // Change to 2000L (2s), etc.

// Maximum retry delay
private val maxReconnectDelayMs = 30000L  // Change to 60000L (60s), etc.

Exponential Backoff Formula

delay = min(
    baseReconnectDelayMs * (2 ^ reconnectAttempts),
    maxReconnectDelayMs
)

Example with current settings:

  • Attempt 1: min(1000 * 2^0, 30000) = 1000ms
  • Attempt 2: min(1000 * 2^1, 30000) = 2000ms
  • Attempt 3: min(1000 * 2^2, 30000) = 4000ms
  • Attempt 4: min(1000 * 2^3, 30000) = 8000ms
  • Attempt 5: min(1000 * 2^4, 30000) = 16000ms
  • Attempt 6: min(1000 * 2^5, 30000) = 30000ms (capped)
  • Attempts 7-10: 30000ms (capped)

Troubleshooting

App not reconnecting

Check logs:

adb logcat | grep -E "(GatewayClient|MainScreen)"

Possible causes:

  • Max retries exceeded (restart app)
  • shouldReconnect set to false
  • Handler not scheduled

Reconnecting too slowly

Reduce backoff delays:

private val baseReconnectDelayMs = 500L  // 500ms instead of 1s
private val maxReconnectDelayMs = 10000L  // 10s instead of 30s

Too many reconnection attempts

Reduce max attempts:

private val maxReconnectAttempts = 5  // 5 instead of 10

Memory leaks

Ensure cleanup:

  • Check disconnect() calls resetReconnectionState()
  • Verify reconnectHandler?.removeCallbacksAndMessages(null)
  • Look for uncancelled handlers in logs

Future Enhancements

Potential improvements:

  • Jitter - Add randomness to backoff to prevent thundering herd
  • Network detection - Skip retries when network is unavailable
  • Manual retry button - User can trigger reconnection after max retries
  • Smarter reset - Reset counter after stable connection (e.g., 5 minutes)
  • Configurable via settings - User-adjustable retry behavior
  • Background reconnection - Continue attempting when app is backgrounded

Version

1.0.0 - Initial auto-reconnection implementation (February 2026)