Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agents/reticulum.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Deep subsystem reference for AI assistants. Open this when a task touches the Re
- **Deep links / QR:** OS scheme is **`lxm://`** (not `mesh-client://`); `MeshClientDeepLinkHost`, `meshClientDeepLink.ts` (`lxmPaperMessage` kind + `looksLikeLxmPaperBlob`; Games `lxm://game/<session>` / Ratspeak `lrgp:<session>` → `lxmGameSession`), `handleReticulumQrIngest.ts` (shared Network/Chat/OS paper + in-app contact ingest), `applyLxmPaperIngest` → `POST /api/v1/lxmf/paper/ingest`, `QrIngestControl` / `QrCodeImage`. OS contact / MeshCore imports confirm before upsert; **paper OS deep links ingest without confirm**; Games session links open Reticulum Games tab via `openReticulumGameSession`.
- **Decommissioned hubs:** `src/shared/reticulumDecommissionedHubs.ts` (Amsterdam only) — stack-start auto-disable + **Add default backbones** disables matching enabled TCP rows; UI badge + enable-block in `ReticulumInterfacesPanel.tsx` (`isDecommissionedReticulumTcpInterfaceRow`); keep TS↔Rust synced via `pnpm run check:reticulum-decommissioned-hubs`. Default backbone picker + region-grouped interface list (Primary & Global / North America / Europe / Asia & Oceania / Specialty / User Defined) in `reticulumDefaultHubPresets.ts` + `ReticulumDefaultHubsPickerModal.tsx`; muted disabled rows + checkbox bulk delete; `countEnabledDefaultHubPresets` / >3 enable warning
- **BLE RNode RSSI:** `useReticulumBleRnodeRssiMap` gates on sidecar **running** (not api-ready), burst-then-steady scans via nested `acquireReticulumBleScan`, clears sticky targets immediately when all BLE RNodes are disabled
- **Propagation mode / sync:** Network → Propagation nodes owns Off/Auto/Manual (default **Off**; persisted values including legacy App-panel `auto` are honored). Auto one-time syncs the best Discovered PN by destination hash (no Add, no Preferred write) via `startPropagationSyncCascade` + sidecar `destination_hash` sync, then configured remotes, then local-prop (skips remotes when no enabled interfaces); runtime hook `useReticulumPropagationAutoSync`. Manual uses Preferred, else picks the best configured remote **for that sync only** (no Preferred write), then the remaining remotes, then local-prop. Off = **no PN support**: `startPropagationSyncCascade` returns early (per-row Sync is disabled in UI), `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` are false, `ReticulumPropagationNotice` is hidden, and the sidecar disarms the outbound PN plus empties cascade candidates (`propagation_mode` in `mesh_client_stack.json`, `POST /api/v1/propagation/mode`, `candidates_for_propagation_mode`); renderer pushes the mode on change and on sidecar-ready. `reticulumPropagationStore` / `reticulumPropagationSync.ts` — Complete on HaveAll, Establishing stall (~45s) + hard ceiling (~180s), auto-sync interval from last success with failure cooldown, error keys for identity / non-PN / peering stamp; stamps `lastPropagationSyncAttemptAt` / `activePropagationSyncAttemptAt` for WS correlation. **Nothing-to-sync is not a failure:** when the cascade contacts no node it writes `syncNoTarget` / `syncLocalLoading` (never overwriting a real error from an attempted node), the local row reports sidecar `status: "loading"` while the messagestore reads (`local_propagation_status` + `PropagationBridge::messagestore_load_pending`, per-row Sync disabled), and the 30 s tick calls `refreshFromSidecar` while `hasPropagationCascadeCandidate` is false so a fresh stack recovers on its own — `refreshFromSidecar` must **not** clear the active attempt while `sync.active`. Debug snapshot `propagationClient` exposes mode/preferred/autoTarget/resolvedSyncTargetId. **Auto also deposits on Discovered PNs:** sidecar `auto_discovered_candidates` (`pn_cascade.rs`, Auto only, cap 3, hop-sorted, skips inactive / self / already-configured / over `max_peering_cost`) appends after configured remotes and before local-prop, rebuilt from the shared `rebuild_pn_cascade_candidates` helper in `live.rs` (called by `refresh_pn_cascade_candidates` **and** the PN announce handler); `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` therefore count discovered rows in Auto, so the Chat notice hides and the link-timeout failure bridge holds off. **Chat notice dismiss:** `chatNoticeDismissed` (`mesh-client:reticulumPropagationNoticeDismissed`) with **Don't show again** on the banner and **Show propagation reminder in Chat** in the Network section. **Named sync target:** `startSync` stamps `syncTargetId`; progress line, inline error, and Sync toasts resolve it with `resolveReticulumPropagationTargetLabel`; the cascade clears it when nothing was contacted so `syncNoTarget` / `syncLocalLoading` stay unprefixed. **Attempts settle before the cascade advances:** `startSync` returns `accepted` | `deferred` | `failed` (not a boolean) — only sidecar _acceptance_ starts `awaitPropagationSyncSettled` (terminal WS frame or stall/ceiling watchdog). `failed` advances with ~15 min session-memory omit via `reticulumPropagationSyncBackoff.ts`; `deferred` (`PROPAGATION_SYNC_OUTBOUND_BUSY` — outbound deposit owns the PN link) advances **without** backoff so the next tick may retry; `cancelled` (user Cancel) stops; `success` ends the run. Remote steps are capped by `PROPAGATION_CASCADE_BUDGET_MS` (5 min) then fall through to local-prop; each remote attempt is capped by `PROPAGATION_CASCADE_ATTEMPT_TIMEOUT_MS` (~60s); local fallback refreshes nodes when local looks disabled; the cascade is single-flight (`resetPropagationSyncCascadeState` is the test seam) so overlapping 30 s ticks join one run while an explicit per-row Sync supersedes it. Auto `/api/v1/interfaces` probe **fails open** (assumes interfaces enabled) so a broken proxy still tries remotes before local. **Retrieval vs peer sync:** the WS progress bar / HaveAll reflects the `/offer` peer sync (`PropagationSyncTask`, inventory replication only). The half that actually pulls your mail into Chat is the **client `/get` download** (`PropagationClient`), wired in `propagation_download.rs` + `propagation_bridge.rs::poll_client_download` and driven by `live.rs::spawn_client_download_driver` (list → get → purge; decrypt with local identity → router delivery callback). `local-prop` Sync uses `PropagationBridge::drain_local_inbox` (in-process node `/get` replay). Logs: real inbox retrieval is `propagation-retrieve` (`retrieve_mode=get|local`, `listed`/`downloaded`/`delivered`); the peer-offer outcome is `propagation-sync … peer_outcome=have_all|transfer` (**not** retrieval).
- **Propagation mode / sync:** Network → Propagation nodes owns Off/Auto/Manual (default **Off**; persisted values including legacy App-panel `auto` are honored). Auto one-time syncs via `startPropagationSyncCascade` + sidecar `destination_hash` sync in order: **finite-hop discovered** (no Add/Preferred) → **configured remotes** → **unknown-hop discovered** → local-prop (skips remotes when no enabled interfaces); runtime hook `useReticulumPropagationAutoSync`. Sidecar `start_propagation_sync` hard-fails with `PROPAGATION_PATH_UNKNOWN` when `ensure_path_for_direct` fails after announce settle (same code as offer probe — no Establishing stall). Manual uses Preferred, else picks the best configured remote **for that sync only** (no Preferred write), then the remaining remotes, then local-prop. Off = **no PN support**: `startPropagationSyncCascade` returns early (per-row Sync is disabled in UI), `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` are false, `ReticulumPropagationNotice` is hidden, and the sidecar disarms the outbound PN plus empties cascade candidates (`propagation_mode` in `mesh_client_stack.json`, `POST /api/v1/propagation/mode`, `candidates_for_propagation_mode`); renderer pushes the mode on change and on sidecar-ready. `reticulumPropagationStore` / `reticulumPropagationSync.ts` — Complete on HaveAll, Establishing stall (~45s) + hard ceiling (~180s), auto-sync interval from last success with failure cooldown, error keys for identity / non-PN / peering stamp; stamps `lastPropagationSyncAttemptAt` / `activePropagationSyncAttemptAt` for WS correlation. **Nothing-to-sync is not a failure:** when the cascade contacts no node it writes `syncNoTarget` / `syncLocalLoading` (never overwriting a real error from an attempted node), the local row reports sidecar `status: "loading"` while the messagestore reads (`local_propagation_status` + `PropagationBridge::messagestore_load_pending`, per-row Sync disabled), and the 30 s tick calls `refreshFromSidecar` while `hasPropagationCascadeCandidate` is false so a fresh stack recovers on its own — `refreshFromSidecar` must **not** clear the active attempt while `sync.active`. Debug snapshot `propagationClient` exposes mode/preferred/autoTarget/resolvedSyncTargetId. **Auto also deposits on Discovered PNs:** sidecar `auto_discovered_candidates` (`pn_cascade.rs`, Auto only, cap 3, hop-sorted, skips inactive / self / already-configured / over `max_peering_cost`) appends after configured remotes and before local-prop, rebuilt from the shared `rebuild_pn_cascade_candidates` helper in `live.rs` (called by `refresh_pn_cascade_candidates` **and** the PN announce handler); `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` therefore count discovered rows in Auto, so the Chat notice hides and the link-timeout failure bridge holds off. **Chat notice dismiss:** `chatNoticeDismissed` (`mesh-client:reticulumPropagationNoticeDismissed`) with **Don't show again** on the banner and **Show propagation reminder in Chat** in the Network section. **Named sync target:** `startSync` stamps `syncTargetId`; progress line, inline error, and Sync toasts resolve it with `resolveReticulumPropagationTargetLabel`; the cascade clears it when nothing was contacted so `syncNoTarget` / `syncLocalLoading` stay unprefixed. **Attempts settle before the cascade advances:** `startSync` returns `accepted` | `deferred` | `failed` (not a boolean) — only sidecar _acceptance_ starts `awaitPropagationSyncSettled` (terminal WS frame or stall/ceiling watchdog). `failed` advances with ~15 min session-memory omit via `reticulumPropagationSyncBackoff.ts`; `deferred` (`PROPAGATION_SYNC_OUTBOUND_BUSY` — outbound deposit owns the PN link) advances **without** backoff so the next tick may retry; `cancelled` (user Cancel) stops; `success` ends the run. Remote steps are capped by `PROPAGATION_CASCADE_BUDGET_MS` (5 min) then fall through to local-prop; each remote attempt is capped by `PROPAGATION_CASCADE_ATTEMPT_TIMEOUT_MS` (~60s); local fallback refreshes nodes when local looks disabled; the cascade is single-flight (`resetPropagationSyncCascadeState` is the test seam) so overlapping 30 s ticks join one run while an explicit per-row Sync supersedes it. Auto `/api/v1/interfaces` probe **fails open** (assumes interfaces enabled) so a broken proxy still tries remotes before local. **Retrieval vs peer sync:** the WS progress bar / HaveAll reflects the `/offer` peer sync (`PropagationSyncTask`, inventory replication only). The half that actually pulls your mail into Chat is the **client `/get` download** (`PropagationClient`), wired in `propagation_download.rs` + `propagation_bridge.rs::poll_client_download` and driven by `live.rs::spawn_client_download_driver` (list → get → purge; decrypt with local identity → router delivery callback). `local-prop` Sync uses `PropagationBridge::drain_local_inbox` (in-process node `/get` replay). Logs: real inbox retrieval is `propagation-retrieve` (`retrieve_mode=get|local`, `listed`/`downloaded`/`delivered`); the peer-offer outcome is `propagation-sync … peer_outcome=have_all|transfer` (**not** retrieval).
- **PN hosting:** Network **Advanced PN hosting** / `ReticulumPnHostingDangerZone`; shared `pnHostingPolicy.ts` + sidecar `pn_hosting_policy.rs` / `pn_hosting_apply.rs`; `POST /api/v1/propagation/hosting-policy`; rsLXMF policy-setters overlay ([ratspeak/rsLXMF#6](https://github.com/ratspeak/rsLXMF/pull/6)). Messagestore loads in background on live attach; enabled `local-prop` serve/announce waits until load completes.
- **Interface modes:** rnsd `mode` via `reticulumInterfaceMode.ts` + sidecar `normalize_interface_mode` (keep catalogs in sync — `pnpm run check:reticulum-interface-modes` in pre-commit/`release.sh`); add defaults TCP/UDP/I2P → `boundary`, RNode → `access_point`; UI in `ReticulumInterfacesPanel`; default hub presets add/repair missing mode to `boundary` (do not overwrite valid non-boundary). See [../reticulum.md#interface-modes](../reticulum.md#interface-modes).
- **Share instance defaults:** missing keys bootstrap to `share_instance = No` / `instance_name = mesh-client` (does not overwrite explicit Yes/`default`); SharedInstanceClient banner + `disable_share_instance` repair; offline lint via `reticulum:validateConfig` / Network **Check config** / `pnpm run reticulum:config:check`
Expand Down
40 changes: 29 additions & 11 deletions reticulum-sidecar/src/stack/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3559,9 +3559,9 @@ impl LiveBridge {
// Re-apply persisted PN pubkey before identity wait (announce flood may have evicted it).
self.rehydrate_propagation_identities_from_persisted();
// Link proofs are ignored unless the destination pubkey is in known_identities.
// Resolve identity (+ path when possible) before Establishing, same as LXMF delivery.
// Resolve identity before Establishing; hard path gate comes after announce settle
// and before peering PoW (never start Establishing / stamp work without a path).
let identity_ok = self.ensure_identity_for_direct(&dest_hex).await;
let _path_ok = self.ensure_path_for_direct(&dest_hex, true).await;
let identity_known_after = self
.outbound
.lock()
Expand All @@ -3581,15 +3581,6 @@ impl LiveBridge {
}
return Err("PROPAGATION_TARGET_NOT_PN".into());
}
let peering = match self.resolve_propagation_peering(&dest_hex).await {
Ok(p) => p,
Err(e) => {
if let Ok(mut driver) = self.outbound.lock() {
driver.set_propagation_sync_target(None);
}
return Err(e);
}
};
// Fresh LXMF delivery announce so the PN can return LRPROOF (reverse path).
let announced = self
.ensure_lxmf_announce_for_propagation_sync(&dest_hex)
Expand Down Expand Up @@ -3620,7 +3611,33 @@ impl LiveBridge {
);
return Err("PROPAGATION_SYNC_OUTBOUND_BUSY".into());
}
// Hard path gate after announce settle so RequestPath can benefit from the announce.
// Do this before peering PoW — nonzero peering_cost stamps are expensive and useless
// when there is no path (would previously burn CPU then stall into syncTimedOut).
let path_ok = self.ensure_path_for_direct(&dest_hex, true).await;
let hops = self.hops_to_destination(&dest_hex).await;
if !path_ok {
if let Ok(mut driver) = self.outbound.lock() {
driver.set_propagation_sync_target(None);
}
tracing::info!(
target: "propagation-sync",
dest = %dest_hex,
path_ok,
hops = ?hops,
"propagation sync aborted — no path to PN"
);
return Err("PROPAGATION_PATH_UNKNOWN".into());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let peering = match self.resolve_propagation_peering(&dest_hex).await {
Ok(p) => p,
Err(e) => {
if let Ok(mut driver) = self.outbound.lock() {
driver.set_propagation_sync_target(None);
}
return Err(e);
}
};
// Pin PN pubkey for the duration of Establishing so announce-flood eviction
// cannot drop it before LRPROOF validation (see known_identities cap).
let pinned = if let Ok(mut driver) = self.outbound.lock() {
Expand All @@ -3636,6 +3653,7 @@ impl LiveBridge {
tracing::info!(
target: "propagation-sync",
dest = %dest_hex,
path_ok,
hops = ?hops,
pinned,
"starting remote propagation sync"
Expand Down
39 changes: 39 additions & 0 deletions reticulum-sidecar/src/stack/propagation_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,45 @@ mod tests {
live.contains("spawn_client_download_driver") && live.contains("start_client_download"),
"live sync must drive the client `/get` download"
);
// Remote sync must hard-fail when no path exists (same as offer probe) instead of
// starting Establishing and timing out in the renderer.
let sync_fn_start = live
.find("pub async fn start_propagation_sync")
.expect("start_propagation_sync");
let sync_fn = &live[sync_fn_start..];
let sync_fn_end = sync_fn[1..]
.find("\n pub ")
.map_or(sync_fn.len(), |idx| idx + 1);
let sync_body = &sync_fn[..sync_fn_end];
assert!(
sync_body.contains("PROPAGATION_PATH_UNKNOWN"),
"start_propagation_sync must return PROPAGATION_PATH_UNKNOWN when path is missing"
);
assert!(
!sync_body.contains("let _path_ok"),
"start_propagation_sync must not discard ensure_path_for_direct"
);
let path_gate_at = sync_body
.find("PROPAGATION_PATH_UNKNOWN")
.expect("PATH_UNKNOWN in start_propagation_sync");
let peering_at = sync_body
.find("resolve_propagation_peering")
.expect("peering resolve in start_propagation_sync");
let start_sync_at = sync_body
.find("start_sync(hash")
.expect("start_sync call in start_propagation_sync");
assert!(
path_gate_at < peering_at,
"path gate must run before peering PoW (nonzero peering_cost)"
);
assert!(
peering_at < start_sync_at,
"peering must run after path succeeds and before start_sync"
);
assert!(
path_gate_at < start_sync_at,
"path gate must run before start_sync / Establishing"
);
}

/// Auto deposits on discovered PNs, so a newly heard announce must refresh the cascade
Expand Down
6 changes: 6 additions & 0 deletions src/main/support-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ describe('extractLxmfOutboundLogSlice', () => {
'debug peer refresh ok',
'info target=propagation-deposit outbound PN deposit Completes',
'info target=propagation-retrieve sync transfer progress',
'info target=propagation-sync propagation sync aborted — no path to PN',
'warn propagation establish failed: NoLinkProof',
'error PROPAGATION_PATH_UNKNOWN',
].join('\n'),
'utf8',
);
Expand All @@ -117,6 +120,9 @@ describe('extractLxmfOutboundLogSlice', () => {
expect(slice).toContain('DeliverPropagated');
expect(slice).toContain('propagation-deposit');
expect(slice).toContain('propagation-retrieve');
expect(slice).toContain('propagation-sync');
expect(slice).toContain('propagation establish');
expect(slice).toContain('PROPAGATION_PATH_UNKNOWN');
expect(slice).not.toContain('hello world');
expect(slice).not.toContain('peer refresh ok');
});
Expand Down
3 changes: 3 additions & 0 deletions src/main/support-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export function extractLxmfOutboundLogSlice(...logChunks: Buffer[]): Buffer {
/lxmf-outbound/i,
/propagation-deposit/i,
/propagation-retrieve/i,
/propagation-sync/i,
/propagation establish/i,
/PROPAGATION_PATH_UNKNOWN/i,
/LXMF advancing PN cascade/i,
/LXMF outbound delivery failed/i,
/Direct path failover/i,
Expand Down
Loading