Reticulum sidecar IPC contract
HTTP + WebSocket on 127.0.0.1 (ephemeral port in production; default dev port 19437).
Aligned with Ratspeak ratspeak-tauri commands — not meshchat aiohttp.
Electron main validates proxy paths: must start with /api/v1/ (no .. segments).
REST
Status and app
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/status |
{ status, version, rns_ready, lxmf_ready } — see listen-first note below |
|
| GET | /api/v1/app/info |
{ sidecar_version, rns_version?, lxmf_version? } |
Listen-first / ready flags: status: "ok" means the HTTP server is accepting connections (Electron health poll succeeds). Cold start clears persisted ready bits; rns_ready / lxmf_ready stay false until attach_live finishes (RNS/LXMF bridge up). TCP/API clients can hit /api/v1/status and identity routes while live attach (path table, BLE Peer, PN messagestore) continues in the background.
Discovery persistence and shutdown
Nomad and RRC announces update the in-memory lists and WebSocket events immediately. Their cached metadata is saved together at most once per minute; idle intervals do not write. Explicit user changes still save immediately and include pending discoveries. State saves synchronize file contents and close the temporary writer before replacing the complete JSON atomically, then synchronize the containing directory on POSIX systems. Windows has no portable directory synchronization through Rust's standard library, so rename durability across power loss depends on the OS; complete-file replacement still protects against process interruption.
POST /api/v1/stack/flush-state returns { ok: true } after saving pending discoveries, or { ok: false, error } on failure. It leaves the live stack and GATT sessions running. Electron uses it before Quit with a five-second deadline to allow large state files to serialize and synchronize, then continues terminating the process even if persistence fails. Timeout logs identify the endpoint and its elapsed budget. A crash, forced kill, or failed flush can lose discoveries since the last successful save; user settings retain their existing immediate-save behavior.
POST /api/v1/stack/prepare-stop flushes before transport teardown and again after stopping producers. Electron's normal Stop request keeps a one-second deadline. Quit can abort that request and issue a separate flush; the sidecar's shared write lock orders saves even if the original HTTP request is canceled. Soft restart uses the same detach path. Failed periodic saves retain pending state and retry at the next interval.
Failures before file replacement still return an error so callers can roll back their in-memory mutation. Once replacement succeeds, a directory synchronization failure leaves the change committed in memory and on disk, logs a durability warning, and retains a pending retry. The live stack retries directory synchronization at the next persistence interval or shutdown flush without rewriting unchanged JSON; a subsequent user save also retries. Until synchronization succeeds, power-loss durability remains uncertain.
Identity
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/identity/status |
{ configured, identity_hash, lxmf_hash, display_name?, public_key? } |
|
| POST | /api/v1/identity/register-known |
{ destination_hash, public_key } |
{ ok } (registers 64-byte pubkey for Direct LXMF / Columba QR) |
| POST | /api/v1/identity/generate |
{ display_name?, replace? } |
{ ok, mnemonic?, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/import |
{ mnemonic, display_name?, replace? } |
{ ok, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/import-backup |
{ backup, passphrase?, display_name?, replace? } — Ratspeak .rsi (ratspeak.identity.v2 + PIN) or ratspeak.identity.v1 |
{ ok, identity_hash, lxmf_hash, display_name? } (restores private key) |
| POST | /api/v1/identity/import-private |
{ private_key, display_name?, replace? } — hex / base64 / URL-safe base64 / base32 / raw key text |
{ ok, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/export |
{ passphrase } — PIN ≥ 6; Ratspeak-compatible .rsi |
{ ok, backup, file_name? } |
| POST | /api/v1/identity/export-raw |
{ passphrase } — PIN ≥ 6 (same gate as .rsi); official 64-byte Reticulum identity |
{ ok, raw: { data_base64, data_hex, data_base32, file_name, identity_hash, lxmf_hash, format } } (UI save path uses data_base64 only) |
| POST | /api/v1/identity/display-name |
{ display_name } |
{ ok } |
Interfaces
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/interfaces |
{ interfaces: [], primary_local_serial_interface_id?, effective_primary_local_serial_interface_id? } |
|
| POST | /api/v1/interfaces |
{ type, name?, host?, port?, preset?, serial_port?, callsign?, mode? } |
{ ok, interface? } |
| POST | /api/v1/interfaces/primary-local-rnode |
{ id } — enabled locally connected serial interface (rnode, rnode_multi, kiss over USB, BLE, or local TCP) |
{ ok, reordered?, effective_id?, error? } |
| PUT | /api/v1/interfaces/{id} |
Partial patch (see below) | { ok, interface? } |
| DELETE | /api/v1/interfaces/{id} |
{ ok } |
|
| POST | /api/v1/interfaces/{id}/enable |
{ ok } |
|
| POST | /api/v1/interfaces/{id}/disable |
{ ok } |
|
| GET | /api/v1/rnode/presets |
{ presets: [] } |
|
| GET | /api/v1/serial/ports |
{ ports: [] } |
|
| GET | /api/v1/ble/availability |
{ available, missing, permissions_granted, probe_failed? } |
|
| GET | /api/v1/ble/scan |
timeout_secs (1–30, default 5), mode (peer | rnode | all) |
{ devices: [{ address, name?, rssi?, kind? }] } or { ok: false, error } |
| POST | /api/v1/ble/handle-ltk-desync |
{ address, name?, error? } — MAC / UUID / ble://…; name (OS Bluetooth display name) is required on macOS when address is a CoreBluetooth UUID; optional driver error string |
{ ok, device_address, bond_purged, message, purge_error? } + WS BleLtkDesync |
LoRa GATT (Meshtastic / MeshCore)
The sidecar GATT module owns the Meshtastic/MeshCore btleplug central. rsReticulum uses separate BLE centrals in the same process. Electron main proxies LoRa GATT via gatt-sidecar-proxy.ts / gatt:* IPC.
ensureForBle() starts the process with --ble-only: HTTP/GATT are available, but configured Reticulum interfaces, Nomad hosting, and rncp listeners remain stopped. Electron's Reticulum status reports running: false, processRunning: true. Explicit Reticulum Start promotes the same process through /api/v1/stack/start, leaving GATT sessions connected. Without an identity, Start opens the setup shell and returns identity_required: true; HTTP ready flags remain false until an identity is configured and the live stack attaches. Repeated Start calls reuse the live stack or retry a failed attachment. Watchdog recovery preserves BLE-only mode.
The GATT registry rejects conflicting owners within this API (mac_conflict) and serializes its scans (scan_busy). Electron's coexistence coordinator also checks configured Reticulum addresses and serializes app-requested scans and LoRa connection setup. Direct HTTP callers and autonomous rsReticulum scans do not participate in the Electron lease; the external-registration endpoints do not register RNode/Peer connections automatically. Stable error codes include adapter_missing, permission_denied, scan_busy, mac_conflict, connect_timeout, gatt_discover_failed, pairing_required, bond_removed, write_failed, session_not_found, and feature_disabled.
Write requests contain one complete ToRadio protobuf or MeshCore command, at most 512 bytes. The sidecar preserves the characteristic value, prefers a write with response when supported, and leaves ATT long writes to the OS. btleplug does not expose negotiated MTU.
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/gatt/availability |
{ available, missing, permissions_granted, probe_failed? } |
|
| GET | /api/v1/gatt/scan |
mode (meshtastic | meshcore), timeout_secs |
{ ok, devices: […] } or { ok: false, code, error, owner? } |
| POST | /api/v1/gatt/release-central |
Electron main only (RNode bond recovery). Destructive: closes every LoRa GATT session and drops the btleplug central | { ok, sessions_closed } or error |
| POST | /api/v1/gatt/clear-bond-recovery |
Electron main only. Clears the bond-recovery hold so Meshtastic/MeshCore may recreate a central | { ok } |
| GET | /api/v1/gatt/pair-state |
address. Windows only (WinRT); elsewhere unsupported |
{ ok, paired } or { ok: false, code, error } |
| POST | /api/v1/gatt/pair |
{ address, pin } (PIN 4–6 digits, never logged). Windows only; mac_conflict while a session holds the address |
{ ok } or authentication_failed / pairing_required / … |
| POST | /api/v1/gatt/unpair |
{ address }. Windows only; mac_conflict while a session holds the address |
{ ok } or error |
| POST | /api/v1/gatt/sessions |
{ profile, address } |
{ ok, sessionId, mtu? } or { ok: false, code, error, owner? } |
| DELETE | /api/v1/gatt/sessions/{session_id} |
{ ok } or error |
|
| POST | /api/v1/gatt/sessions/{session_id}/write |
{ data_b64 } |
{ ok } or error |
| GET | /api/v1/gatt/sessions/{session_id}/rssi |
{ ok, rssi } |
|
| GET | /api/v1/gatt/sessions/{session_id}/connected |
{ connected } |
|
| GET | /api/v1/gatt/sessions/{session_id}/events |
WebSocket | session events (bytes, disconnected, rssi, error, …) |
| POST | /api/v1/gatt/registry/register |
{ profile, address } — external owner (e.g. RNode). No in-app caller; reserved for external owners such as rsReticulum RNode BLE |
{ ok } or mac_conflict |
| POST | /api/v1/gatt/registry/unregister |
{ profile, address } — no in-app caller (see register) |
{ ok } |
PUT /api/v1/interfaces/{id} patch fields (all optional): name, type, enabled, host, port, preset, serial_port, frequency, bandwidth, txpower, spreading_factor, coding_rate, callsign, id_interval, mode (full | point_to_point | access_point | roaming | boundary | gateway; aliases ap/gw; empty clears; invalid non-empty → API error invalid interface mode: …; omitted on PUT preserves existing), discoverable, latitude, longitude, height, discovery_name, announce_interval_min, connectable, reachable_on, discovery_lxmf_address, discovery_stamp_value, discovery_encrypt, publish_ifac, discovery_frequency, discovery_bandwidth, discovery_spreading_factor, discovery_coding_rate, network_name, passphrase, flow_control, bootstrap_only. On POST add, omitted mode defaults to boundary (tcp/udp/i2p), gateway (backbone), or access_point (rnode/rnode_multi); Auto/BLE Peer/KISS/Pipe leave mode unset. Backbone uses catalog fields listen_on (extra_config) + port.
The Connection tab UI edits a subset: name and mode for all types; host / port for TCP/UDP; serial_port, preset, callsign for RNode; catalog-driven fields for Backbone / serial / AX.25 / Local; bootstrap_only for temporary bridge interfaces. Enable/disable uses the dedicated POST routes.
Config and stack settings
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/config |
{ content } |
|
| PUT | /api/v1/config |
{ content } (full rnsd INI text) |
{ ok } |
| GET | /api/v1/config/export |
{ content } |
|
| POST | /api/v1/config/import |
{ content, mode: merge\|replace } |
{ ok, warnings? } |
| GET | /api/v1/config/audit |
{ issues: ConfigAuditIssue[] } — config vs live interface audit |
|
| POST | /api/v1/config/repair |
{ repair_kinds?: string[] } — repair_config, apply_preset, add_auto, disable_share_instance (empty = all) |
{ ok, repaired: string[], restart_required: bool } |
| GET | /api/v1/stack/settings |
{ enable_transport, share_instance, loglevel, announce_interval_sec, autoconnect_discovered_interfaces, required_discovery_value, interface_discovery_sources, network_identity } — announce_interval_sec defaults to 3600 (1 h) when absent from config; consume knobs default to 0 / 16 / "" / "" |
|
| PUT | /api/v1/stack/settings |
Partial StackSettings JSON — omitted fields keep on-disk values (atomic merge under sidecar lock) |
{ ok } — send only fields to change; do not GET→merge→PUT from the client |
| POST | /api/v1/stack/start |
Attach live Reticulum to an existing BLE-only process; reuse an already attached stack. Without an identity, keep the setup shell available. | { ok, rns_ready?, identity_required?, error? } |
| POST | /api/v1/stack/restart |
Reload live Reticulum while preserving HTTP and GATT. | { ok, error? } |
| POST | /api/v1/stack/flush-state |
Electron main before Quit (5 s deadline). Saves pending discoveries; stack and GATT keep running (see above). | { ok } or { ok: false, error } |
| POST | /api/v1/stack/prepare-stop |
Electron main on Stop (1 s deadline). Flushes before transport teardown and again after producers stop (see above). | { ok } or { ok: false, error } |
| DELETE | /api/v1/announces |
{ ok } — clears stub persisted peers; live path table may repopulate under rns-stack |
|
| POST | /api/v1/announces |
{ ok } — send LXMF delivery announce now (live stack). Interval scheduling also sends startup + periodic announces from announce_interval_sec |
Config bootstrap (stack start): When announce_interval_sec is missing from rnsd config, the sidecar writes 3600; explicit 0 is left unchanged (ensure_announce_interval_sec_default in reticulum-sidecar/src/stack/config.rs). Missing share_instance / instance_name are filled as No / mesh-client (explicit values are preserved). Same bootstrap pass may set discover_interfaces = Yes for RMAP ingest.
Path medium preference and pins
Routing bias between RF (LoRa / RNode) and network (TCP/UDP/I2P/gateway/shared-instance) path slots. Backed by rsReticulum TransportQuery::SetPathMediumPreference / SetPeerMediumPin / GetPathSlots; persisted in mesh_client_stack.json as path_medium_preference (default "lowest") and peer_medium_pins ({ "<32 hex dest>": "rf" | "network" }, max 256 entries).
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/settings/path-medium-preference |
{ ok, preference: "lowest"\|"network"\|"rf", pins: { "<hash>": "rf"\|"network" } } |
|
| PUT | /api/v1/settings/path-medium-preference |
{ preference: "lowest"\|"network"\|"rf" } |
{ ok, preference }; 400 { ok: false, error: "invalid_path_medium_preference" } on an unknown token. On success emits path_medium_preference WS ({ preference }) |
| GET | /api/v1/peers/{hash}/paths |
{ ok, destination_hash, preference, pin, effective_preference, live, paths: PathSlot[] }; 400 on a non-32-hex hash |
|
| PUT | /api/v1/peers/{hash}/medium-pin |
{ pin: "rf"\|"network"\|null } (null clears) |
{ ok, destination_hash, pin }; 400 "pin_required" (key absent), "invalid_pin", or bad hash. Emits peers_updated WS |
PathSlot fields: active (route currently used for outbound), hops, via_hash (immediate transport id, may be null), interface (live interface name), interface_id, medium (rf / network), timestamp, expires, expired. Slots are ranked active-first and capped by rsReticulum MAX_PATH_SLOTS (3).
preference vs effective_preference: preference is the persisted global setting and pin the persisted per-destination override; effective_preference is what the live transport actually applies for that destination (pin resolved against the global) and is null when the stack is not live.
Offline / persist behavior: When the stack is down, PUTs persist and are applied on the next live start (LiveBridge::spawn re-applies the preference — skipped when it is the default lowest — then every pin). While the stack is live, PUTs persist only if the live apply succeeds; a failed live apply rolls back the persisted value so disk/UI cannot drift ahead of the transport. GET …/paths returns live: false with an empty paths array when there is no live transport. GET /api/v1/peers stays active-route-only and does not embed path arrays; fetch slots per destination.
preference semantics (rsReticulum): lowest applies no medium bias and ranks purely by hops; network / rf are "prefer if possible" — when the preferred medium has no live slot the other medium becomes active without clearing the preference, so the preferred medium can reclaim the route later.
LXMF and contacts
| Method | Path | Body / notes | Response |
|---|---|---|---|
| POST | /api/v1/lxmf/send |
{ destination_hash, text, reply_to_hash?, reply_to_id?, reply_preview_text? } |
Live: stamps LXMF FIELD_REPLY_TO (0x30) / optional FIELD_REPLY_QUOTE (0x31) before sign; { ok, delivery_method?, delivery_status?, sent_via?, message? } or { ok: false, error: "no_propagation_node" }. delivery_status on this response is initial enqueue state only (queued or sending) — not delivery confirmation. Before live attach (rns-stack without bridge): fail-closed { ok: false, error: "lxmf send requires live rns-stack sidecar" }. Stub build: { ok, sent_via?, message? } |
| POST | /api/v1/lxmf/paper/create |
{ destination_hash, text, reply_to_hash?, reply_to_id?, reply_preview_text? } (Chat UI currently sends destination_hash + text only) |
Live: signed DeliveryMethod::Paper + to_paper_uri (encrypt to peer pubkey in known_identities); no network send. { ok, uri, message_hash, delivery_method: "paper", message? } or { ok: false, error: "identity_unknown" \| "paper_too_large" \| "identity_not_configured" \| "invalid_hash" \| "internal_error" }. Emits WS lxmf_message outbound with delivery_method/sent_via/received_via=paper and delivery_status: "delivered" — does not emit lxmf_outbound_status. Stub: { ok: false, error: "identity_not_configured" } |
| POST | /api/v1/lxmf/paper/ingest |
{ uri } (lxm:// base64url paper blob; URI length capped before decrypt) |
Live: from_paper_uri + local identity decrypt + ingest_lxm_uri → delivery callback / WS lxmf_message inbound (delivery_method/received_via=paper). { ok, message? } or { ok: false, error: "invalid_uri" \| "decrypt_failed" \| "identity_not_configured" \| "paper_too_large" \| "internal_error" }. Stub: { ok: false, error: "identity_not_configured" } |
| POST | /api/v1/lxmf/reaction |
{ destination_hash, target_hash, emoji } |
Live: stamps standard LXMF FIELD_REACTION (0x40) msgpack map (REACTION_TO 0x00 = 32-byte parent hash, REACTION_CONTENT 0x01 = emoji) before sign and keeps the emoji as message content; an unparsable target_hash fails open (sends content-only, no 0x40). { ok, message? }. Before live attach: fail-closed { ok: false, error: "lxmf reaction requires live rns-stack sidecar" } |
| GET | /api/v1/lxmf/recent |
?since_ts= (ms, optional), ?since_seq= (opaque ring_seq, optional), ?limit= (default 200, max 500) |
{ messages: [], ring_len } — ring buffer of recent inbound LXMF payloads for WS lag/reconnect catch-up (not durable across sidecar restart; capped at 200). Rows are chronological (oldest→newest) and each accepted row is stamped with monotonic ring_seq. Cursor: since_ts alone keeps timestamp > since_ts; with since_seq, keep rows after the complete (since_ts, since_seq) cursor (timestamp > since_ts or same-ms with ring_seq > since_seq) so same-ms twins remain recoverable without reprocessing the boundary; ring_len is current buffer occupancy |
| DELETE | /api/v1/lxmf/messages/{hash} |
{ ok } |
|
| GET | /api/v1/contacts |
{ contacts: [] } — overlays announce/peer/Nomad labels onto nameless or hash-prefix contact display_name values (does not overwrite a real name) and may persist fills |
|
| DELETE | /api/v1/contacts |
{ ok, cleared } — clears LXMF contacts after demoting them into the peer cache (keeps Peers; does not delete chat messages) |
Peers, topology, and propagation
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/peers |
?refresh=1 optional |
{ peers: [] } — live path table when rns-stack enabled; without refresh=1 may serve a short-TTL maintenance cache; refresh=1/true forces live GetPathTable (manual Refresh). display_name overlayed from contacts/Nomad/announce label cache; optional public_key (128 hex) when known from announces |
| POST | /api/v1/peers/{hash}/path |
{ ok } — emits peers_updated WS on success |
|
| POST | /api/v1/peers/{hash}/probe |
{ ok, hops? } live; { ok, mode, hash } stub — emits peers_updated on success |
|
| POST | /api/v1/ping |
{ destination_hash } |
{ ok, rtt_ms? } |
| POST | /api/v1/maintenance/path-table |
{ ok, cleared } or { ok: false, error } — drops every cached route from the RNS path table (renderer reticulumClearPathTable.ts) |
|
| GET | /api/v1/topology |
{ nodes, edges, total?, shown?, truncated? } — via_hash is the immediate RNS next hop (transport id); sidecar infers self → relay when needed |
|
| GET | /api/v1/rmap/discovered |
{ discovered: RmapDiscoveredWireRow[] } — local RMAP v4 heard interfaces (7-day TTL eviction in rsReticulum DiscoveryStore) |
RmapDiscoveredWireRow fields (see src/shared/reticulum-types.ts): discovery_hash, transport_id, network_id, discovery_name, interface_type, latitude, longitude, height, transport_enabled, reachable_on, port, ifac_netname, ifac_netkey, LoRa RF fields (frequency, bandwidth, spreading_factor, …), hops, stamp_value, discovered, last_heard, heard_count, status (available/stale/unknown), has_coordinates. Renderer caps at 2,000 newest rows with client-side TTL eviction.
WS rmap.discovery: sidecar polls DiscoveryStore every 10s; emits full { discovered: [...] } snapshot when JSON fingerprint changes. Stub builds return { discovered: [] }.
| GET | /api/v1/packets | ?limit=500 (1–2500) | { packets: [] } — recent wire tap ring buffer |
| DELETE | /api/v1/packets | | { ok } — clear wire tap buffer |
| GET | /api/v1/propagation | | { propagation, preferred_id, auto_sync_interval_sec, propagation_mode, propagation_auto_blacklist, pn_hosting_policy, last_propagation_sync_at? } — local-prop rows include message_count, storage_bytes when live |
| GET | /api/v1/propagation/discovered | | { discovered: DiscoveredPropagationRow[] } — heard lxmf.propagation announces (not auto-configured) |
| POST | /api/v1/propagation/add | { destination_hash, name?, skip_probe? } | { ok, node } or { ok: false, error } — probes /offer unless skip_probe; may return PROPAGATION_OFFER_UNSUPPORTED, PROPAGATION_PEER_COST_EXCEEDS_MAX, identity/path errors |
| POST | /api/v1/propagation/hosting-policy | PnHostingPolicy | { ok } — persist + apply local PN hosting / peering policy |
| PUT | /api/v1/propagation/{id} | { name } | { ok } — rename a remote node (local-prop rejected) |
| DELETE | /api/v1/propagation/{id} | | { ok } — remove a remote node (local-prop rejected; clears preferred if that id) |
| POST | /api/v1/propagation/{id}/enable | | { ok } — for local-prop, starts PN serve + announce (waits for messagestore load — see below) |
| POST | /api/v1/propagation/{id}/disable | | { ok } — for local-prop, stops PN serve + announce |
| POST | /api/v1/propagation/{id}/preferred | | { ok } |
| POST | /api/v1/propagation/mode | { mode: "off"\|"auto"\|"manual" } | { ok } — gates outbound PN cascade + Auto sync eligibility |
| POST | /api/v1/propagation/auto-blacklist | { destination_hash } | { ok } — Ignore for Auto (sync + deposit); 32-hex; cap 256 |
| DELETE | /api/v1/propagation/auto-blacklist/{hash} | | { ok } — Allow Auto again |
| POST | /api/v1/propagation/sync | { propagation_id? } or { destination_hash? } | { ok } — client /get-primary for remotes; local-prop drains in-process. Errors include PROPAGATION_PATH_UNKNOWN, PROPAGATION_RETRIEVE_BUSY, PROPAGATION_SYNC_OUTBOUND_BUSY, PROPAGATION_STACK_NOT_LIVE, identity/non-PN/peering codes |
| POST | /api/v1/propagation/sync/cancel | | { ok } — cancels Sync UI run and aborts in-flight client /get (abort_transfer) |
| POST | /api/v1/propagation/auto-sync-interval | { interval_sec } | { ok } — 0 disables periodic sync; persists with stack |
Deferred PN messagestore: On live attach, the local propagation messagestore loads in the background (spawn_blocking) so a large disk store does not gate TCP/LXMF/RRC readiness. New writes still go to storage_dir. If local-prop is enabled, serve/announce starts only after that load finishes (avoids advertising an empty PN while the scan runs).
PnHostingPolicy (mirrored in src/shared/pnHostingPolicy.ts / sidecar pn_hosting_policy.rs):
| Field | Default | Notes |
|---|---|---|
peering_cost |
18 |
Must be ≤ max_peering_cost |
max_peering_cost |
26 |
|
autopeer |
true |
|
autopeer_maxdepth |
4 |
Cap 64 |
max_peers |
20 |
1–256 |
propagation_stamp_cost |
16 |
|
propagation_stamp_flex |
3 |
Must be ≤ stamp cost |
message_storage_limit_mb |
256 |
1–10240 |
propagation_limit_kb |
256 |
1–102400 |
sync_limit_kb |
10240 |
1–102400 |
delivery_limit_kb |
1000 |
1–102400 |
from_static_only |
false |
|
auth_required |
false |
|
enforce_stamps |
false |
Persisted for UI/compat; not applied on current rsLXMF tip (set_enforce_stamps removed upstream; stamp gating uses set_stamp_requirements) |
enforce_ratchets |
false |
Persisted for UI/compat; not applied on current rsLXMF tip (set_enforce_ratchets removed upstream) |
static_peers |
[] |
Lowercase 32-hex hashes (max 256) |
node_name |
null |
Trimmed; max 128 scalar chars; no control chars |
pn_announce_interval_sec |
360 |
Cap 86400 |
announce_at_start |
true |
Nomad Network
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/nomadnetwork/nodes |
{ nodes: [] } |
|
| POST | /api/v1/nomadnetwork/nodes/favorite |
{ hash, favorited } |
{ ok } |
| POST | /api/v1/nomadnetwork/nodes/identify |
{ destination_hash, identify } |
{ ok } — persist per-node identify; turning off closes any identified cached Link (future connections only) |
| POST | /api/v1/nomadnetwork/nodes/identify/clear |
{} |
{ ok, cleared } — turn identify off for every node |
| GET | /api/v1/nomadnetwork/page/{hash}?path=… |
optional data, force_path_refresh=true (stale-path retry), identify=true |
page payload |
| GET | /api/v1/nomadnetwork/file/{hash}?path=… |
optional force_path_refresh=true, identify=true |
{ ok, file_name?, content_base64? } |
| GET | /api/v1/nomadnetwork/media/{hash}?path=… |
optional force_path_refresh=true, identify=true |
Inline page media (e.g. images) fetched from a remote node; { ok: false, error } without the live rns-stack |
| GET | /api/v1/nomadnetwork/serving |
{ ok, serving } (local host status; includes content_source, content_layout, watcher_status, last_error) |
|
| PUT | /api/v1/nomadnetwork/serving |
{ enabled, display_name? } |
{ ok, serving } |
| PUT | /api/v1/nomadnetwork/serving/content-source |
{ path: string } |
{ ok, serving } — set watched folder (site root or pages dir); required before start; restarts host if running |
| GET | /api/v1/nomadnetwork/serving/pages |
{ ok, pages: [] } — My Pages list; pages can also be edited on disk in the watched folder |
|
| PUT | /api/v1/nomadnetwork/serving/pages |
{ path, content } |
{ ok } — My Pages New page / Edit (MicronPageEditor) |
| DELETE | /api/v1/nomadnetwork/serving/pages?path=… |
{ ok } — My Pages Delete |
|
| GET | /api/v1/nomadnetwork/serving/page?path=… |
{ ok, path, content } |
|
| GET | /api/v1/nomadnetwork/serving/acl?path=… |
{ ok, path, exists, content } — page's {page}.allowed list; path is the page (e.g. index.mu), never the .allowed file; missing list → exists: false, content: "" |
|
| PUT | /api/v1/nomadnetwork/serving/acl |
{ path, content } |
{ ok } — write {page}.allowed beside an existing page (64 KiB cap); My Pages Access |
| DELETE | /api/v1/nomadnetwork/serving/acl?path=… |
{ ok } — remove {page}.allowed (page becomes public); missing list is a no-op |
|
| GET | /api/v1/nomadnetwork/serving/files |
{ ok, files: [] } — My Pages lists these read-only |
|
| PUT | /api/v1/nomadnetwork/serving/files |
{ path, content_base64 } |
{ ok } — sidecar-only; not exposed in the My Pages UI |
| DELETE | /api/v1/nomadnetwork/serving/files?path=… |
{ ok } — sidecar-only; not exposed in the My Pages UI |
identify defaults to false: Nomad Links are anonymous unless the caller passes identify=true, which sends LINKIDENTIFY on a new Link. The sidecar keys its cached Nomad Link on destination and identify, so a change never reuses a Link with the other identification. The renderer sends the flag from the node's persisted identify field (NomadNodeRow.identify in GET /nodes rows). The nomadnetwork.node WS event carries identity_hash but not the flag.
Auto-restore order when the live stack comes up: load nomad_serving_content_source → if nomad_serving_enabled and a content source is set, start hosting → start FS watcher on pages/ (and files/ when that directory exists). If enabled without a content source, set last_error=content_source_required and do not start. Other failures keep enabled=true with running=false and last_error set; logs use the [nomad-serving] tag.
RRC (Reticulum Relay Chat)
Multi-hub sessions are keyed by lowercase 32-hex hub_dest_hash. Soft cap: 8 simultaneous hubs. Nick / room / body lengths are capped at the API boundary.
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/rrc/hubs |
{ hubs: RrcHubInfo[] } |
|
| POST | /api/v1/rrc/hubs |
{ dest_hash, label?, favorited? } |
{ ok, hub? } |
| POST | /api/v1/rrc/hubs/favorite |
{ dest_hash, favorited } — dest_hash must be exact 32 hex |
{ ok } |
| POST | /api/v1/rrc/connect |
{ dest_hash, nickname? } |
{ ok } / { ok: false, error } |
| POST | /api/v1/rrc/disconnect |
{ dest_hash? } — omit/empty tears down every hub |
{ ok } |
| GET | /api/v1/rrc/status |
{ sessions: [], identity_hash } |
|
| POST | /api/v1/rrc/join |
{ hub_dest_hash, room, key? } |
{ ok } |
| POST | /api/v1/rrc/part |
{ hub_dest_hash, room } |
{ ok } |
| POST | /api/v1/rrc/send |
{ hub_dest_hash, room?, body, type?, dst_hash? } |
{ ok } |
| POST | /api/v1/rrc/nick |
{ nickname, hub_dest_hash? } — omit hub applies to every session |
{ ok } |
| GET | /api/v1/rrc/rooms |
?hub_dest_hash= optional |
{ rooms: [] } |
Typed renderer wrappers: electronAPI.reticulum.rrc.* (listHubs, upsertHub, setFavorite, connect, disconnect, getStatus, join, part, send, setNickname, getRooms).
Remote — rnsh (shell) and rncp (file transfer)
Stock Reticulum utility clients hosted by the sidecar. Paths are filesystem paths only (never file bytes in JSON). Electron gates rncp.send / rncp.fetch / rncp.listener mutations behind picker-allowlisted IPC (electronAPI.reticulum.rncp.*).
| Method | Path | Body / notes | Response |
|---|---|---|---|
| POST | /api/v1/rnsh/connect |
{ destination_hash } |
{ ok, session_id?, identity_hash?, fingerprint?, error? } |
| POST | /api/v1/rnsh/input |
{ session_id, data, encoding? } — encoding: "base64" optional |
{ ok, error? } |
| POST | /api/v1/rnsh/resize |
{ session_id, rows?, cols? } |
{ ok, error? } |
| POST | /api/v1/rnsh/disconnect |
{ session_id } |
{ ok, error? } |
| GET | /api/v1/rnsh/status |
{ sessions: [] } |
|
| POST | /api/v1/rncp/send |
{ destination_hash, path } |
{ ok, transfer_id?, error? } |
| POST | /api/v1/rncp/fetch |
{ destination_hash, remote_path, save_path? } |
{ ok, transfer_id?, error? } |
| POST | /api/v1/rncp/cancel |
{ transfer_id } |
{ ok, error? } |
| POST | /api/v1/rncp/accept |
{ transfer_id } |
{ ok, … } / { ok: false, error } |
| POST | /api/v1/rncp/reject |
{ transfer_id } |
{ ok, error? } |
| GET | /api/v1/rncp/status |
{ transfers: [] } |
|
| POST | /api/v1/rncp/announce |
{} |
{ ok } / { ok: false, error: "listener_not_enabled" } |
| GET | /api/v1/rncp/listener |
{ enabled, destination_hash?, inbound_mode, allowed, blocked } |
|
| POST | /api/v1/rncp/listener |
{ enabled, save_dir?, allow_fetch?, fetch_jail?, overwrite?, allowed?, blocked? } |
{ ok, error? } |
| POST | /api/v1/remote/path-capability |
{ destination_hash } |
PathCapability JSON (speed, via_atoms, transfer_allowed, shell_allowed, reason_key?) |
| GET | /api/v1/remote/identity |
{ identity_hash, rncp_receive_hash? } |
WS event types (in addition to the list below): rnsh.stdout, rnsh.stderr, rnsh.status, rnsh.closed, rnsh.error, rncp.offer, rncp.progress, rncp.completed, rncp.failed, rncp.cancelled. Payloads always include session_id or transfer_id.
Caps (local policy): max 8 shells, max 3 concurrent transfers, default 25 MiB file size; LoRa/BLE-only destinations refuse transfers (path_constrained) before opening a link.
Listener persistence: a successful POST /api/v1/rncp/listener stores the config (rncp_listener_* keys in mesh_client_stack.json); the live stack restores the listener on start, so the inbound mode survives sidecar/app restarts. enabled: false persists only the flag and keeps the last dir/policy fields.
System
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/diagnostics |
Reticulum-native health snapshot — includes announce_ws coalesce pressure (last_window_ingress / unique / overflow, last_storm_at_ms, last_flush_at_ms). Renderer Diagnostics emits reticulum/announce-bus-pressure when lag/storm/overflow is recent. |
|
| POST | /api/v1/system/factory-reset |
{ ok } — Electron UI must call electronAPI.reticulum.factoryReset (generic proxyPost blocks this path) |
|
| GET | /api/v1/voice/status |
LXST telephony status (available, enabled, running, microphone_muted, active_call) |
|
| POST | /api/v1/voice/call |
{ identity_hash } |
Place Opus call (QualityHigh, ~15s discovery). identity_hash is 32-hex identity (not LXMF dest). |
| POST | /api/v1/voice/answer |
Answer incoming call | |
| POST | /api/v1/voice/reject |
Reject ringing call | |
| POST | /api/v1/voice/hangup |
End active call | |
| POST | /api/v1/voice/mute |
{ muted } |
Renderer mute flag (sidecar drops PCM ingest) |
| POST | /api/v1/voice/audio |
{ profile?, channels, samples_b64 } |
Push one PCM frame (LE f32 base64) for Opus TX. Only established calls transmit; earlier frames are accepted-and-dropped as not_established (soft-drop — do not fatal). Renderer must defer capture/TX until voice.update status established (Answer only warms AudioContext). Use dedicated IPC reticulum:voiceSendAudio (own ~2000/min budget); generic reticulum:proxyPost rejects this path so realtime PCM does not starve the shared 900/min proxy ceiling. |
| POST | /api/v1/voice/memo/start |
Start LXMF voice-memo encode session (AM_OPUS_OGG / QualityMedium). Dedicated IPC reticulum:voiceMemoStart — generic proxyPost rejects /api/v1/voice/memo/*. |
|
| POST | /api/v1/voice/memo/audio |
{ session_id, channels, samples_b64 } |
Push one 24 kHz / 60 ms (1440 f32) PCM frame for memo encode. Dedicated IPC reticulum:voiceMemoSendAudio (rate-limited like live voice PCM). |
| POST | /api/v1/voice/memo/stop |
{ session_id } |
Finish encode; returns { ok, ogg_base64, duration_ms, size_bytes, mode }. Cap ~240 KiB. Dedicated IPC reticulum:voiceMemoStop. |
| POST | /api/v1/voice/memo/cancel |
{ session_id } |
Drop memo session without producing Ogg. Dedicated IPC reticulum:voiceMemoCancel. |
| GET | /api/v1/games/status |
LRGP live status (available, enabled, running, registered apps). Use dedicated IPC reticulum:gamesStatus — generic proxyGet rejects /api/v1/games/* |
|
| GET | /api/v1/games/apps |
Registered game manifests (ttt, chess) | |
| GET | /api/v1/games/sessions |
optional ?peer= |
Session list (sidecar LrgpStore) |
| GET | /api/v1/games/sessions/:id |
Session detail + board metadata; JSON may include overlay delivery_state (sending / propagating / failed / …) |
|
| POST | /api/v1/games/action |
{ dest_hash, app_id, command, session_id?, payload? } |
Send LRGP action (challenge/accept/move/…). Dedicated IPC reticulum:gamesAction (~600/min own bucket). Enqueue failure emits games.action_result { ok: false } after sidecar rollback |
| POST | /api/v1/games/sessions/:id/resend |
Resend last envelope (same nonce). Envelope bytes persist in companion games_outbound.db across sidecar restart |
|
| POST | /api/v1/games/sessions/:id/read |
Mark session read | |
| DELETE | /api/v1/games/sessions/:id |
Delete session | |
| GET | /api/v1/identities |
{ identities: […] } — slots under config/identities/<id>/ + active_identity; working key remains config/identity; flat identity migrates to identities/default/ |
|
| POST | /api/v1/identities |
{ display_name? } |
{ ok, id, identity } or { ok: false, error } — rns-stack only; stage slot → apply working key → commit pointer last (rollback on failure). Cap 16 slots. Errors: identity_slot_limit_reached, display_name_*. Emits restart. |
| POST | /api/v1/identities/switch |
{ identity_id } |
{ ok } or { ok: false, error } — stash, install target → working, reconcile, then commit pointer. Errors: identity_slot_not_configured, identity_not_found |
| POST | /api/v1/identities/delete |
{ identity_id } |
{ ok } or { ok: false, error } — refuses cannot_delete_active_identity / cannot_delete_last_identity |
WebSocket
GET /ws — server push JSON text frames:
{ "type": "lxmf_message", "payload": { ... } }
Event types: lxmf_message, lxmf_outbound_status, events_lagged (WS subscriber skipped N broadcast frames — client should GET /api/v1/lxmf/recent), announce.received, peers_updated, path_medium_preference (global preference changed; payload { preference }), stats_update, interface.state, stack_restart_requested, propagation_sync, propagation.discovered (heard lxmf.propagation announce), resource.received, rmap.discovery (payload { discovered: RmapDiscoveredWireRow[] }), nomadnetwork.node (Nomad peer announce heard), nomad.serving_start / nomad.serving_stop (local hosting lifecycle; payload includes destination_hash / display_name on start — renderer currently polls serving status via HTTP), BleLtkDesync (BLE RNode LTK / Peer-removed recovery; payload { device_address, bond_purged, message, purge_error? } — emitted after POST /api/v1/ble/handle-ltk-desync), RRC: rrc.hub, rrc.connected, rrc.disconnected, rrc.room.joined, rrc.room.parted, rrc.message, rrc.error, plus Remote: rnsh.stdout / rnsh.stderr / rnsh.status / rnsh.closed / rnsh.error, rncp.offer / rncp.progress / rncp.completed / rncp.failed / rncp.cancelled, plus LXST voice signalling: voice.update / voice.incoming / voice.terminated / voice.error / voice.stats, plus LRGP games: games.update / games.action_result (turn-based; stay on shared /ws).
Note: Live wire_packet frames are not pushed on /ws (they starved critical lxmf_message events on large meshes). Sniffer/Stats poll GET /api/v1/packets while those panels are mounted. PacketTap rows still feed the sidecar packet log and LXMF egress evidence.
GET /ws/voice: dedicated high-rate stream for LXST PCM receive frames (voice.audio with link_id, profile, channels, samples_b64). Same starvation rationale as packet tap — do not put realtime audio on shared /ws. Electron main bridges this to reticulum:voiceAudio (not reticulum:event).
-
rrc.disconnected: payload{ hub_dest_hash, reason, will_reconnect? }. Whenwill_reconnectisfalse(orreasonislocal_disconnect), the renderer drops that hub session. Whentrue(or omitted on older sidecars), the UI shows reconnecting and keeps volatile rooms until WELCOME. -
Outbound Direct backchannel: On live stack start,
LinkDeliveryManager::set_inbound_packet_sender(spawn_lxmf_outbound_backchannel(...))forwards plaintext on outbound-initiated reusable Direct links into the same unpack path as peer-initiatedlxmf.delivery. Developer log marker:LXMF outbound-link backchannel packet. Without this wiring, the peer's first reply may Ack on their client but never appear in mesh-client Chat. lxmf_outbound_status: authoritative outbound delivery updates for network sends. Payload:{ message_hash, status, delivery_method?, to_hash?, sent_via? }wherestatusisdelivered,stored_locally,failed, or intermediatesending(egress upgrade or Direct→PN cascade step). mesh-client mapsdelivered/stored_locally→ UI Completes (acked) and persistsdelivery_status(+delivery_methodwhen present) to SQLite; remote PN Completes (delivered) show Stored at propagation node; local-prop Completes (stored_locally) mean deposited on your hosted PN (full PN parity — peer/offersync may still propagate; house badge ≠ dead-end outbox);failed→ Failed. Do not treat/api/v1/lxmf/sendresponsedelivery_status(queued/sending) as terminal. After Direct exhausts, the sidecar cascades preferred remote → other enabled remotes (hop-sorted) → local-prop last, emittingsending+delivery_method: "propagated"or"stored_locally"between attempts before a finaldelivered/stored_locally/failed. Paper create/ingest does not use this event — Completes vialxmf_messagewithdelivery_method: "paper"/delivery_status: "delivered".announce.received: coalesced WS notify for LXMF identity announces / path responses (named or nameless). Sidecar applies identity-key + display-name cache updates immediately, but emits at most one WS frame per coalesce window (500ms normal / 1000ms when >256 distinct destinations are pending) so announce storms stay O(1) bus pressure on large meshes (~100k). Payload is either a single{ destination_hash, display_name?, hops, aspect?, identity_hash? }(legacy / one-row flush) or{ announces: [{ destination_hash, display_name?, hops, aspect?, identity_hash? }, ...] }(capped at 1024, named preferred; overflow dropped — slow peer poll recovers).aspectis set when announcename_hashmaps to a known app name (lxmf.delivery,lxmf.propagation,nomadnetwork.node,rrc.hub,lxst.telephony); omitted for path responses / unknown hashes (clients must not invent"unknown").identity_hashis the hex identity recovered from the validated announce when present. Each flush publishes pressure counters underGET /api/v1/diagnostics→announce_ws(ingress/unique/overflow + storm/flush timestamps) for the Diagnosticsreticulum/announce-bus-pressurewarning. Display names update the peer-label cache only — announces do not auto-create LXMF contacts. That cache is overlayed ontoGET /api/v1/peers/ topology rows and onto nameless/hash-prefix rows fromGET /api/v1/contacts(list_contactsmay persist those fills) so path-table and contact refreshes keep announce aliases.peers_updated: also emitted when the live path table gains new destination hashes (maintenance tick). Payload may include{ added: string[], patches: PeerRow[], count }(added/patches capped at 1024). Renderer applies patches incrementally, including route-field changes. A full peer dump is used on connect, manual Refresh, restart, safety poll, or apeers_updatedpayload that cannot be applied incrementally:cleared,demoted_from_contacts, or a single-hashprobe/path event. Hop/timestamp-only churn does not emit.
lxmf_message payload fields include sender_hash, text, timestamp, message_hash, optional direction (inbound / outbound), optional delivery_status (sending on optimistic outbound rows; delivered on paper Completes), optional reply_to_hash / reply_preview_text (from LXMF FIELD_REPLY_TO / FIELD_REPLY_QUOTE), optional reaction_target (from LXMF FIELD_REACTION 0x40, present as lowercase 64-hex when decoded — a reaction takes precedence over a reply, so reply_to_hash is omitted when reaction_target is set, and the field emoji fills text only when the message content is empty), and transport markers received_via / sent_via. When FIELD_REACTION is absent or malformed the payload is unchanged (no reaction_target; reply/plain paths intact). Outbound sent_via is path-table / PacketTap evidence, not “any local RNode enabled”: atomic values are rf, ble, tcp, network, or paper (offline QR handoff); multi-egress observes join with + (e.g. rf+tcp, ble+network). Inbound received_via uses the path-table interface name matched to local interface config (same atoms — so a TCP hub named “RNS Testnet” is tcp, not network) or paper for decrypted paper URIs. Never use Meshtastic-style both for Reticulum network egress (legacy both may still appear in SQLite allowlists).
lxmf_outbound_status payload: message_hash, status (delivered / stored_locally / failed / sending), optional delivery_method, optional sent_via (egress evidence upgrade before Completes).
Electron bridge
Renderer calls electronAPI.reticulum.*; main process proxies to this API (sandboxed renderer cannot reach localhost directly). Lifecycle / proxy / Remote / factory-reset handlers live in src/main/ipc/reticulum-handlers.ts. Reticulum destination / Remote address / inbound-policy DB handlers are in src/main/ipc/reticulum-db-handlers.ts; RRC room history uses src/main/ipc/rrc-db-handlers.ts.
Shared reticulum:proxy* IPC is capped at 900/min. GET /api/v1/lxmf/recent uses a dedicated 120/min bucket so WS-lag catch-up does not starve mesh control. On rate-limit errors the renderer applies exponential backoff (reticulumProxyRateLimitBackoff.ts).
| IPC channel | Role |
|---|---|
reticulum:start / stop / getStatus |
Sidecar lifecycle |
reticulum:syncInterfaceIssueScope |
Drop TCP/TX latch entries for disabled/removed interfaces; sticky enabled-name filter for later log lines |
reticulum:clearBleBondIssuesForOnlineInterfaces |
Clear bleBondRemoved / blePairingTimedOut latches after named interfaces report online |
reticulum:proxyGet / proxyPost / proxyPut / proxyDelete |
HTTP proxy to paths above (shared 900/min; lxmf/recent 120/min) |
reticulum:factoryReset |
Factory reset (generic proxyPost blocks /api/v1/system/factory-reset; UI must use this channel) |
reticulum:validateConfig |
One-shot validate-config --json against userData/reticulum/config (read-only; safe while stack runs) |
reticulum:readDefaultConfigFile |
Read first existing system rnsd config path |
reticulum:showConfigImportDialog |
Native file picker for config import |
reticulum:showIdentityImportDialog |
Native file picker for 64-byte private key (.retid, .key, …) |
reticulum:showNomadContentSourceDialog |
Native folder picker for Nomad My Pages content source (site root or pages/ dir); records picker allowlist |
reticulum:setNomadContentSource |
Apply Nomad watched content source; path must match last folder-picker result (blocks arbitrary proxyPut) |
reticulum:rncpSend / rncpFetch / setRncpListener |
Picker-gated rncp send/fetch/listener (path must match reticulum-remote-paths allowlist) |
reticulum:showRncpOpenFileDialog / showRncpSaveDirectoryDialog |
Native pickers that seed the rncp send-file / save-dir+fetch-jail allowlists |
reticulum:revealInFolder |
Reveal a path in the OS file manager when it matches an rncp picker allowlist |
reticulum:event / reticulum:status |
Shared /ws events and sidecar status (preload: onEvent / onStatus) |
reticulum:voiceSendAudio |
Dedicated PCM TX ingest (POST /api/v1/voice/audio); own ~2000/min budget (not generic proxyPost) |
reticulum:voiceAudio |
Dedicated /ws/voice PCM frames (voice.audio; preload: onVoiceAudio) |
electronAPI.reticulum.voice.* |
Preload surface: getStatus / call / answer / reject / hangup / mute / sendAudio |
reticulum:voiceMemoStart / voiceMemoSendAudio / voiceMemoStop / voiceMemoCancel |
Dedicated LXMF voice-memo encode IPC; generic proxy rejects /api/v1/voice/memo/* |
electronAPI.reticulum.voiceMemo.* |
Preload surface: start / sendAudio / stop / cancel |
reticulum:gamesStatus / gamesApps / gamesSessions / … |
Dedicated LRGP games IPC (~600/min); generic proxy rejects /api/v1/games/* |
electronAPI.reticulum.games.* |
Preload: getStatus / listApps / listSessions / getSession / sendAction / resend / markRead / deleteSession |
getStatus / onStatus may include interfaceIssueAlert (TCP connect failures, TX queue drops — including BLE / bond-stale cause keys used by Diagnostics and Connection hints, link-delivery timeouts, transport saturation / slow queries, bleBondRemoved stale RNode bonds (cleared when that interface reports online again, on stack stop / interface remove, or after the 5‑minute stale window), blePairingTimedOut OS passkey / TX-read timeouts). Per-entry latch timestamps use a 5-minute stale window (RETICULUM_INTERFACE_ISSUE_ALERT_STALE_MS) for most issues; Connection syncs enabled interface names via syncInterfaceIssueScope so disabling or removing an interface clears that name immediately and rejects re-latch from lagging log lines. After a BLE RNode recovers, clearBleBondIssuesForOnlineInterfaces clears bond/pairing latches immediately. Stopping the stack (or unexpected process exit) clears the tracker.
propagation_sync WebSocket payload: { active: boolean, progress: number, message: string | null }. Progress uses 0–100 driven by the client /get download (Establishing ≈10 … Complete ≈100). Sticky success emits active:false, progress:100; cancel/stall/failure emit active:false, progress:0 (and must not emit a trailing 100). Sync POST /api/v1/propagation/sync may return PROPAGATION_PATH_UNKNOWN, PROPAGATION_RETRIEVE_BUSY, PROPAGATION_SYNC_OUTBOUND_BUSY, PROPAGATION_STACK_NOT_LIVE, PROPAGATION_IDENTITY_UNKNOWN, PROPAGATION_TARGET_NOT_PN, PROPAGATION_PEERING_STAMP_FAILED, PROPAGATION_PEER_COST_EXCEEDS_MAX, or LOCAL_PROPAGATION_SYNC_UNSUPPORTED. Add may return PROPAGATION_OFFER_UNSUPPORTED / probe timeout failures.
SQLite chat history uses separate db:* handlers (getReticulumMessages, saveReticulumMessage, searchReticulumMessages, deleteReticulumMessage, destination upserts), not sidecar HTTP. Remote saved addresses / inbound policy and RRC room history also use dedicated db:* handlers (not sidecar HTTP).
| IPC channel | Reticulum maintenance behavior |
|---|---|
db:pruneReticulumDestinationsByCount |
Prunes excess non-favorited destinations by oldest last_heard; favorites are preserved. |
db:deleteReticulumDestinationsByAge |
Deletes non-favorited destinations before a calculated Unix-seconds last_heard cutoff. |
db:pruneReticulumIdentityActivityByAge |
Deletes identity-activity rows before an epoch-milliseconds last_seen cutoff. |
db:upsertReticulumIdentityActivityBatch |
Validates and upserts at most 500 activity rows per call. |
db:listReticulumRemoteAddresses / upsert / delete |
Saved Remote (rnsh/rncp) addresses. |
db:listReticulumInboundPolicy / upsert / delete |
Per-identity inbound allow/block policy for rncp. |
db:listRrcMessages / insertRrcMessage / deleteRrcMessagesByRoom |
Persist / clear RRC room history (rrc_messages). |
db:pruneRrcMessagesByCount / db:pruneRrcMessagesByAge |
RRC retention (default enabled, 10,000 count / 30-day age). |
Reticulum startup maintenance runs the destination age/count prune, Reticulum message retention, and RRC message retention independently. VACUUM runs only after the Reticulum startup prune, never on the six-hour session tick.