# Cleave Plan: LXMF Routing Final Fixes

**Date**: 2025-01-25
**Context**: Final pass to fix LXMF routing issues between styrene nodes and hub

## Problem Statement

The styrene daemon on q502l exhibits three distinct failures observed in `/tmp/styrene-daemon.log`:

1. **"Attempt to register an already registered destination"** - Occurs every 30s during re-announce cycle
2. **"No interfaces could process the outbound packet"** - Occurs when attempting to send LXMF messages
3. **Hub connection status accuracy** - `is_connected` may report True when path no longer exists

## Root Cause Analysis

### Issue 1: Duplicate Destination Registration

**Location**: `daemon.py:254-262` calls `rns_service.create_operator_destination()` on every announce cycle

**Problem**: RNS maintains a global destination registry. Creating a destination with the same `(identity, app_name, aspect)` tuple multiple times raises an error. The code creates a new `RNS.Destination` object every 30 seconds without checking if one already exists.

**Evidence**: 
```
2026-01-25 13:32:23,312 - styrene.services.rns_service - ERROR - Failed to create destination: 'Attempt to register an already registered destination.'
```

### Issue 2: Outbound Packet Failure

**Location**: `lxmf_service.py:162-201` and `auto_reply.py:116-124`

**Problem**: Messages are sent via `router.handle_outbound()` without verifying:
1. A path exists to the destination
2. Interfaces are online and can transmit

The AutoInterface fails (WiFi multicast blocked) which is expected, but the TCPClientInterface should be able to transmit. The "No interfaces could process" error suggests the packet routing layer can't find a valid outbound path.

**Hypothesis**: The LXMF delivery destination hash being announced may not match what senders are using, OR the path discovery hasn't completed before send attempts.

### Issue 3: Stale Connection Status

**Location**: `hub_connection.py:76-81`

**Problem**: `is_connected` returns `self._connected` which is set once during `connect()` when a path is found. It never re-validates. The hub could go offline and `is_connected` would still return True.

## Cleave Decomposition

### Subtask A: Destination Caching (rns_service.py)

**Goal**: Prevent duplicate destination registration by caching created destinations

**Changes**:
1. Add `_destinations: dict[str, RNS.Destination]` cache to `RNSService.__init__`
2. Modify `create_operator_destination()` to check cache before creating
3. Add `get_or_create_destination()` method that returns cached or creates new
4. Add `clear_destinations()` for shutdown cleanup

**Files**: `src/styrene/services/rns_service.py`

**Tests**: `tests/services/test_rns_service.py`
- Test destination caching returns same instance
- Test cache key format
- Test clear_destinations removes entries

### Subtask B: Daemon Announce Refactor (daemon.py)

**Goal**: Use cached destinations for periodic announces instead of recreating

**Changes**:
1. Store operator destination during `start()` initialization
2. Reuse stored destination in `_run_loop()` announce cycle
3. Only recreate if destination is None (e.g., after error)

**Files**: `src/styrene/daemon.py`

**Tests**: `tests/test_daemon.py` (may need creation)
- Test announce reuses destination
- Test announce recovers from None destination

### Subtask C: Hub Connection Health Check (hub_connection.py)

**Goal**: Make `is_connected` reflect actual path availability

**Changes**:
1. Add `_last_path_check: float` timestamp
2. Add `PATH_CHECK_INTERVAL = 30` constant
3. Modify `is_connected` property to periodically re-validate path via `RNS.Transport.has_path()`
4. Add `force_path_check()` method for immediate validation
5. Update `status` property to use refreshed connection state

**Files**: `src/styrene/services/hub_connection.py`

**Tests**: `tests/services/test_hub_connection.py`
- Test is_connected re-checks path after interval
- Test is_connected returns False when path disappears
- Test force_path_check bypasses interval

### Subtask D: LXMF Send Path Validation (lxmf_service.py)

**Goal**: Validate path exists before attempting to send, request path if missing

**Changes**:
1. Add `_ensure_path(destination_hash: bytes) -> bool` method
2. In `send_message()`: call `_ensure_path()` before `handle_outbound()`
3. If no path: request path and return failure with specific error
4. Add `send_with_retry()` method that waits for path before sending

**Files**: `src/styrene/services/lxmf_service.py`

**Tests**: `tests/services/test_lxmf_service.py`
- Test send fails gracefully when no path
- Test send requests path when missing
- Test send_with_retry waits for path

### Subtask E: Auto-Reply Path Handling (auto_reply.py)

**Goal**: Handle path-not-found gracefully in auto-reply responses

**Changes**:
1. Check path exists before sending reply
2. If no path: log warning and skip reply (sender may be unreachable)
3. Add configurable `require_path_for_reply: bool` option

**Files**: `src/styrene/services/auto_reply.py`

**Tests**: `tests/services/test_auto_reply.py`
- Test auto-reply skips when no path to sender
- Test auto-reply logs appropriately

## Dependency Graph

```
     [A: Destination Caching]
              |
              v
     [B: Daemon Announce Refactor]
     
     [C: Hub Connection Health] (independent)
     
     [D: LXMF Send Validation]
              |
              v
     [E: Auto-Reply Path Handling]
```

**Execution Order**:
1. A and C can run in parallel (no dependencies)
2. B depends on A
3. D can run in parallel with A, B, C
4. E depends on D

## Success Criteria

1. **No "already registered" errors** in daemon logs during 5-minute run
2. **No "No interfaces could process"** when hub is reachable
3. **`is_connected` returns False** within 60s of hub going offline
4. **Graceful degradation** when paths unavailable (logged, not crashed)
5. **All existing tests pass** after changes
6. **New tests pass** for added functionality

## Validation Plan

1. Deploy to q502l via git pull
2. Start daemon: `tmux new-session -d -s styrene 'RNS_LOGLEVEL=7 .venv/bin/python -m styrene --headless'`
3. Monitor logs: `tail -f /tmp/styrene-daemon.log`
4. Verify no registration errors over 5 minutes
5. Test message send from hub to q502l
6. Kill hub pod, verify `is_connected` transitions to False
7. Restart hub pod, verify reconnection
