This skill should be used when working on the Strangler Fig migration of reticulum_wrapper.py, creating or modifying rns_api.py, working on RnsApiClient or ChaquopyRnsApiClient in Kotlin, extracting business logic from Python to Kotlin, modifying PythonWrapperManager, working on health monitoring, telemetry collection, RMSP, delivery state machines, message reception, link speed probing, identity file management, event/callback extraction, or any task that touches the Python-to-Kotlin migration boundary. It provides the full migration plan, phase dependencies, thin API surface, and anti-patterns to avoid.
reticulum_wrapper.py is 8,032 lines and has grown from a thin RNS/LXMF wrapper into a massive orchestration layer. ~70% of its code is business logic (delivery state machines, telemetry collection, health monitoring, RMSP, event routing) that doesn't need Python — it only needs to call RNS/LXMF APIs. The remaining ~30% genuinely wraps RNS/LXMF Python APIs.
Goal: Invert the abstraction — create a thin Python API (~15 core RNS/LXMF operations), move all business logic to Kotlin, and eventually delete reticulum_wrapper.py.
Strategy: Strangler Fig — leave reticulum_wrapper.py untouched, build the new system alongside it, redirect callers one piece at a time, delete when dead.
This skill should be used when:
python/rns_api.pyRnsApiClient, ChaquopyRnsApiClient, or RnsResult in Kotlinreticulum_wrapper.py into KotlinPythonWrapperManager.kt to redirect calls to RnsApiClientCheck this section to see what has been completed. Update it as phases are finished.
| Phase | Description | Status | PR |
|---|---|---|---|
| 0 | Foundation — Thin Python API + Kotlin Interface | Not Started | — |
| 1 | Health Monitoring (~130 lines) | Not Started | — |
| 2 | Event/Callback System (~300 lines) | Not Started | — |
| 3 | Telemetry Collection (~400 lines) | Not Started | — |
| 4 | RMSP Map Client (~200 lines) | Not Started | — |
| 5 | Identity File Management (~300 lines) | Not Started | — |
| 6 | Link Speed Probing (~450 lines) | Not Started | — |
| 7 | Delivery State Machine (~1,500 lines) | Not Started | — |
| 8 | Message Reception Pipeline (~800 lines) | Not Started | — |
| Final | Delete reticulum_wrapper.py | Not Started | — |
Goal: Create the new API layer without changing anything existing. Both paths coexist.
| File | Purpose |
|---|---|
python/rns_api.py (~250-400 lines) | Thin Python class exposing only raw RNS/LXMF operations |
app/.../service/rns/RnsApiClient.kt | Kotlin interface mirroring the thin API |
app/.../service/rns/RnsResult.kt | Simple sealed result type |
app/.../service/rns/ChaquopyRnsApiClient.kt | Implementation calling rns_api.py via Chaquopy |
Category Methods
───────── ──────────────────────────────────────────────────────────
Lifecycle initialize(config), shutdown(), get_heartbeat()
Identity create_identity(), recall_identity(hash),
load_identity_from_file(path), save_identity_to_file(key, path)
Path/Transport has_path(hash), request_path(hash), hops_to(hash),
get_next_hop_bitrate(hash)
LXMF send_lxmf_message(dest, key, content, fields, method),
set_outbound_propagation_node(hash),
sync_propagation_messages()
Link establish_link(hash, timeout), get_link_stats(hash)
Announce announce_destination(hash, app_data),
register_announce_handler(aspect)
Callbacks set_delivery_callbacks(on_delivered, on_failed, on_sent),
set_message_received_callback(cb),
set_announce_callback(cb)
None. Old path untouched. This is the foundation — both paths coexist.
ChaquopyRnsApiClient with MockKrns_api.get_heartbeat() through Chaquopy, verify responseWhy first: Smallest extraction, very low risk, validates the dual-path pattern.
HealthCheckManager.getHeartbeat() to call rnsApiClient.getHeartbeat() instead of wrapperManager.withWrapper{}PropagationStateMonitor.kt — Kotlin coroutine that polls propagation state from rns_api, detects changes, broadcasts via CallbackBroadcasterget_propagation_state() -> {state, state_name, progress, messages_received}_heartbeat_loop(), get_heartbeat(), _maintenance_loop(), _check_propagation_state_change() (~130 lines)Why second: The 13 callback registrations are the main coupling mechanism. Extracting them early means every subsequent phase benefits.
RnsEventDispatcher.kt with typed listener interfaces (delivery status, message received, announce, location, reaction, propagation state, alternative relay)rns_api.py receives a single RnsEventDispatcher reference at init, calls it for all eventsset_*_callback() calls from PythonWrapperManagerset_*_callback methods and callback storage (~300 lines)TelemetryCollector.kt — stores peer telemetry, handles TTL cleanup, access control, stream responsesumsgpack, called during message send/receive)set_telemetry_collector_enabled(), _cleanup_expired_telemetry(), _send_telemetry_stream_response(), _store_telemetry_for_collector() (~400 lines)RmspServerRegistry.kt — stores discovered servers, queries by geohash/proximity (pure Kotlin)RmspClient.kt — orchestrates server queries using rnsApiClient.establish_link()rns_api.pyrmsp_client.py (~300 lines)IdentityFileManager.kt — file scanning, path resolution, secure wipe, import/export orchestrationrns_api.pyIdentityManager.kt to use IdentityFileManager instead of wrapperManager.withWrapper{}create_identity(), list_identity_files(), delete_identity_file(), import_identity_file(), export_identity_file(), recover_identity_file() (~300 lines)LinkSpeedProber.kt — orchestration logic (try backchannel -> direct -> establish new -> heuristic fallback)establish_link, get_link_stats) stay in rns_api.pyRoutingManager.probeLinkSpeed() to LinkSpeedProberprobe_link_speed(), link finding/stat helpers (~450 lines)DeliveryStateMachine.kt — opportunistic timeout -> propagation fallback -> relay retry -> alternative relayrns_api.send_lxmf_message() converts to LXMF formatsend_lxmf_message(), send_lxmf_message_with_method(), _on_message_delivered(), _on_message_failed(), _on_message_sent(), on_alternative_relay_received(), opportunistic timer, file notification helpers (~1,500 lines)MessageReceptionRouter.kt — routes incoming messages by type (telemetry, reaction, cease signal, regular message)rns_api.py's _on_lxmf_delivery simplified to: extract raw fields -> convert to JSON -> call single Kotlin callback_on_lxmf_delivery(), field routing, buffering (~800 lines)Once all phases complete:
reticulum_wrapper.py has no live callersPythonWrapperManager.withWrapper{} calls to ChaquopyRnsApiClientreticulum_wrapper.py (8,032 lines)PythonWrapperManager -> ChaquopyLifecycleManager (~300 remaining lines for bridge setup)rmsp_client.pyEnd state: Python layer is a single thin file (rns_api.py, ~250-400 lines), all business logic in testable Kotlin.
| File | Role |
|---|---|
python/reticulum_wrapper.py (8,032 lines) | The monolith being strangled |
app/.../service/manager/PythonWrapperManager.kt (896 lines) | Kotlin call site; shrinks per phase |
app/.../service/di/ServiceModule.kt | DI container; new managers wired here |
app/.../service/manager/EventHandler.kt | Reference pattern for event extraction |
python/rmsp_client.py (646 lines) | Partially extracted in Phase 4 |
python/lxst_modules/call_manager.py (730 lines) | Stays in Python (raw RNS.Link) |
Phase 0 (Foundation)
├── Phase 1 (Health)
├── Phase 2 (Events/Callbacks)
│ ├── Phase 3 (Telemetry)
│ │ ├── Phase 7 (Delivery SM)
│ │ └── Phase 8 (Reception)
│ └── Phase 4 (RMSP)
├── Phase 5 (Identity)
└── Phase 6 (Link Speed)
Phases 1, 5, 6 can run in parallel after Phase 0. Phases 3, 4 require Phase 2. Phases 7, 8 require Phases 2 and 3.
RnsApiClientrns_api.py -> RNSreticulum_wrapper.py until final cleanup (Strangler Fig rule)wrapperManager.withWrapper{} from new Kotlin managers — use RnsApiClientrns_api.py — it's a thin pass-through onlyRnsApiClient interfacebuiltins.callAttr("list", list.toTypedArray()) before passing to Python// In ChaquopyRnsApiClient.kt
override suspend fun getHeartbeat(): RnsResult<HeartbeatData> =
withContext(Dispatchers.IO) {
try {
val result = rnsApi.callAttr("get_heartbeat")
RnsResult.Success(parseHeartbeat(result))
} catch (e: Exception) {
RnsResult.Failure(e)
}
}
# In rns_api.py — THIN, no business logic
class RnsApi:
def get_heartbeat(self):
"""Returns raw heartbeat data from RNS."""
if not self._reticulum:
return {"status": "not_initialized"}
return {
"status": "ok",
"uptime": time.time() - self._start_time,
"transport_enabled": self._reticulum.is_transport_instance,
}
sealed class RnsResult<out T> {
data class Success<T>(val data: T) : RnsResult<T>()
data class Failure(val error: Throwable) : RnsResult<Nothing>()
}
| Phase | Lines Extracted | Risk | Size | Dependencies |
|---|---|---|---|---|
| 0: Foundation | 0 (new code) | Low | Medium | None |
| 1: Health monitoring | ~130 | Very Low | Small | P0 |
| 2: Event/callbacks | ~300 | Low | Medium | P0 |
| 3: Telemetry | ~400 | Low | Medium | P0, P2 |
| 4: RMSP | ~500 | Low | Medium | P0, P2 |
| 5: Identity files | ~300 | Low | Small | P0 |
| 6: Link speed | ~450 | Medium | Medium | P0 |
| 7: Delivery SM | ~1,500 | High | Large | P0, P2, P3 |
| 8: Reception | ~800 | High | Large | P0, P2, P3 |
| Total | ~4,380 |