Skip to content

fix(reticulum): Network PN mode, multi-step sync cascade, Auto discovered deposits, and .rsstack layout - #820

Merged
rinchen merged 14 commits into
mainfrom
fix/reticulum-propagation-mode-network
Aug 8, 2026
Merged

fix(reticulum): Network PN mode, multi-step sync cascade, Auto discovered deposits, and .rsstack layout#820
rinchen merged 14 commits into
mainfrom
fix/reticulum-propagation-mode-network

Conversation

@rinchen

@rinchen rinchen commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Moves Reticulum Propagation mode into Network → Propagation nodes and rebuilds Auto/Manual/Off sync + outbound PN cascade end-to-end (renderer + sidecar). Also relocates the Ratspeak overlay stack to a gitignored .rsstack/ workspace, speeds Disconnect & Quit, and hardens cascade deferral / i18n / docs.

Propagation mode UI & semantics

  • Move Off / Auto / Manual from the App panel into Network → Propagation nodes; remove duplicated App controls (ReticulumPropagationControls / ReticulumAppPanelSection).
  • Default mode is Off (MeshChatX parity); persisted values (including legacy App-panel auto) are honored.
  • Off = no PN support: no periodic/per-row/bottom Sync, no Chat reminder, no outbound Direct→PN cascade; sidecar persists propagation_mode, disarms the outbound PN, and builds an empty cascade.
  • Auto: one-time syncs the best Discovered PN by destination hash (no Add, no Preferred write), then configured remotes, then local-prop; with no enabled interfaces, settles local only. Set preferred / Add & prefer remain available.
  • Manual: syncs Preferred, or picks the closest added remote for that sync only (no Preferred write), then remaining remotes → local-prop.
  • Renderer pushes mode to the sidecar (POST /api/v1/propagation/mode) on change and on sidecar-ready; diagnostic snapshot exposes propagationClient (mode / preferred / autoTarget / resolvedSyncTargetId).

Sync cascade settlement & backoff

  • Cascades wait for each attempt to settle (awaitPropagationSyncSettled) — startSync only means sidecar acceptance, not success.
  • startSync returns accepted | deferred | failed (not a boolean).
  • Failed remotes are omitted ~15 min (reticulumPropagationSyncBackoff, session memory); remote half of a cascade is budgeted (~5 min) with ~60s per-remote attempt cap before falling through to local.
  • Soft defer PROPAGATION_SYNC_OUTBOUND_BUSY advances without 15‑minute backoff so an in-flight outbound deposit does not black-hole the best PN.
  • Single-flight cascade: overlapping 30s auto ticks join one run; explicit Sync supersedes; mode flip mid-run aborts.
  • Cancel / supersede no longer surfaces as “node unreachable”; nothing-to-sync uses syncNoTarget / syncLocalLoading (never overwriting a real per-node error).
  • Local messagestore load exposes sidecar status: "loading"; auto-sync tick refreshes from sidecar while no cascade candidate exists so a fresh stack recovers on its own.
  • Progress / errors / toasts name the sync target via syncTargetId + resolveReticulumPropagationTargetLabel (unprefixed when nobody was contacted).

Auto deposits on Discovered PNs + Chat notice

  • Sidecar Auto outbound cascade appends up to 3 heard-but-not-added Discovered PNs (hop-sorted; skips inactive / self / already-configured / over max peering cost) after configured remotes and before local-prop (auto_discovered_candidates / shared rebuild helper on refresh and PN announce).
  • Renderer hasEffectiveReticulumPropagationTarget / cascade capacity count discovered rows in Auto — Chat “no PN” notice hides and the link-timeout Failed bridge holds off while cascade capacity remains.
  • Chat notice is dismissible (“Don’t show again”) with Network toggle to restore; dismiss persists in localStorage.

Disconnect & Quit performance

  • Quit is a single owner: main stop({ forQuit: true }) skips prepare-stop BLE drain and SIGKILLs after ~750ms (instead of ~1s + 5s graceful wait).
  • Connected Disconnect & Quit no longer awaits renderer onDisconnect (main already tears down BLE/TCP/sidecar); stay-in-app Stop / Disconnect keep the graceful path.

Ratspeak stack layout (.rsstack)

  • Relocate path deps and overlay checkouts from sibling ../../rs* to gitignored .rsstack/ so a standalone rsReticulum mirror stays clean.
  • Update Cargo.toml, sidecar path resolution, clone/ensure/apply/check scripts, update.sh, release build, CI workflows, contract tests, patches README, and docs.
  • Exclude .rsstack/ from repo-wide linters (yamllint / markdownlint / prettier).

Docs, i18n, tooling

  • Align AGENTS.md, docs/reticulum.md, docs/troubleshooting.md with Auto one-time Discovered sync, cascade order (including Auto discovered deposits), settlement/backoff, and Off = no PN.
  • Fix check:i18n-blind locale drift: rewrite modeHelpAuto and syncLocalLoading in all 15 non-en locales; Czech modeHelpManual Příručka:Ručně:.
  • Add check-i18n-quality guards so Preferred-managed Auto help, syncLocalLoading pronoun errors, and Czech handbook false friends fail CI.
  • Remove local pre-push Vitest gate (PR CI already runs the full suite; pre-commit keeps staged tests).
  • Refresh pnpm-lock.yaml via pnpm run update.

Commits (oldest → newest)

  1. f1b5a0e1 — move propagation mode to Network; relocate Ratspeak stack to .rsstack
  2. a807bd11 — PN sync cascade with local Preferred; stop cancel→unreachable
  3. 41e05958 — Auto sync selects without adding PNs or Preferred
  4. b5716504 — Auto one-time syncs best Discovered PN by hash
  5. 54518cc5 — remove local pre-push Vitest gate
  6. 076e7274 — Manual PN sync cascade; Off as no-PN support
  7. a9df6b1b — stop reporting “unreachable” when PN sync has no target
  8. 19ab81db — make Disconnect & Quit exit promptly
  9. 1a67459a — Auto deposits on discovered PNs; dismissible Chat notice
  10. 7fd25e69 — advance PN sync cascade after async attempt failures
  11. 8e61142a — reach local PN after failed Auto sync attempts
  12. 231bb92e — harden cascade deferral, i18n, and helpers
  13. 752eac54 — refresh pnpm-lock after dependency update

Test plan

  • Network → Propagation: cycle Off / Auto / Manual; mode help matches behavior; sidecar receives mode push
  • Off: Sync controls disabled; Chat notice hidden; offline DM Direct exhaustion is terminal Failed (no PN deposit)
  • Auto with a Discovered PN: Sync runs discovered → configured → local without Add/Preferred write; Chat notice hides when Auto has discovered capacity
  • Auto with no interfaces: settles local-prop only
  • Manual: Preferred sync; without Preferred, picks closest added remote for that sync only; failure falls through other remotes → local
  • Cancel mid-sync → cancelled (not unreachable); supersede/delete cancel stays quiet
  • Fresh stack / no targets → syncNoTarget or syncLocalLoading (not “unreachable”); recovers after announce or messagestore load
  • During outbound deposit to a PN: sync soft-defers (OUTBOUND_BUSY) and does not omit that PN for 15 minutes
  • Dead remote: omitted ~15 min; cascade still reaches local-prop
  • Explicit Sync during an in-flight auto cascade supersedes it; flipping mode to Off mid-run stops the cascade
  • Chat notice: Don’t show again persists; Network “Show propagation reminder in Chat” restores it
  • Named sync progress/errors/toasts show the node reached; unprefixed when nobody was contacted
  • Disconnect & Quit with a running sidecar returns promptly; stay-in-app Stop still drains gracefully
  • Developer bundle includes mesh_client_stack.json + lxmf-outbound.log / PN-island diagnostics as documented
  • .rsstack/ clones/overlays apply; sidecar full-feature build works
  • pnpm run check:pr green (incl. new i18n quality guards)

Summary by CodeRabbit

  • New Features

    • Added Off, Auto, and Manual propagation modes with persistent settings.
    • Auto mode can discover and prioritize nearby propagation nodes.
    • Improved propagation sync cascades with target-specific progress, retries, cancellation, and failure handling.
    • Added clearer propagation notices, dismissal controls, target labels, and error messages.
    • Expanded diagnostic and support information for propagation issues.
    • Application exit now shuts down faster without waiting for radio teardown.
  • Documentation

    • Updated setup and troubleshooting guidance for the repository-local stack workspace.

rinchen added 13 commits August 7, 2026 16:52
…tack to .rsstack

Propagation mode (Network + Auto owns Preferred):
- Move Off/Auto/Manual mode selector from the App panel into Network ->
  Propagation nodes; remove the duplicated App controls (announce-only now).
- Default mode is Off (MeshChatX parity). Auto picks the best remote PN at
  pick time (discovered union enabled-configured, lowest hops), soft-upserts +
  sets Preferred without a manual Add, and falls back to enabled local-prop.
- Gate Set preferred / Add & prefer while Auto manages Preferred; Manual/Off
  never auto-write. Periodic auto-sync honors the mode (Off never ticks;
  local-prop still skipped). Mode help text + per-option tooltips added.
- PN-island diagnostics: sidecar deposit-Completes log carries cascade_step +
  delivery_method; diagnostic snapshot gains a propagationClient slice; developer
  bundles always include mesh_client_stack.json + lxmf-outbound.log with expanded
  island log patterns. Docs + troubleshooting (PN-island section + repro matrix).

Relocate Ratspeak overlay stack to repo-local .rsstack workspace:
- Move rns-stack path deps and overlay checkouts from sibling ../../rs* to a
  gitignored .rsstack/ workspace so a standalone rsReticulum mirror stays clean.
- Update Cargo.toml, reticulum-sidecar-path.ts, clone/ensure/apply/check-rsnomad
  scripts, update.sh, build-reticulum-sidecar-release.mjs, all sidecar CI
  workflows, contract tests, patches/README, and docs to the new layout.
- Exclude .rsstack/ from repo-wide linters (yamllint, markdownlint, prettier)
  so the cloned upstream repos are not scanned by mesh-client tooling.
…eachable

Auto tries discovered then configured then local; Manual can Prefer local-only
and falls back to local on remote failure. Delete/supersede cancels no longer
surface as "node unreachable," and the leftover App Reticulum section is gone.
Stop soft-upserting discovered nodes and rewriting Preferred on Auto;
sync configured remotes then local, and surface Sync start/failure feedback.
Auto no longer adds nodes or sets Preferred. Sidecar sync accepts a
destination_hash for a one-time remote sync without mutating the configured
list or Preferred; renderer cascade tries best Discovered by hash, then
configured remotes, then local-prop, and settles local when no interfaces
are enabled.
PR CI already runs the full suite; pre-commit keeps staged tests.
Manual now syncs Preferred, or picks the closest added node for that sync
only (no Preferred write), then falls back through the remaining added
nodes to the local inbox, so a user who never pinned a Preferred node still
gets store-and-forward.

Off previously only stopped periodic sync while Preferred kept carrying
offline DMs. It now means no propagation support end to end: the renderer
blocks every sync path and reports no cascade capacity, and the sidecar
persists propagation_mode, disarms the outbound PN, and builds an empty
cascade so nothing is deposited on a remote PN or the local inbox.
Selecting Auto on a fresh stack could toast "Propagation sync failed — the
node may be unreachable" without contacting anything: no PN announces had
arrived yet, no remotes were added, and the local inbox reports enabled=false
until its deferred messagestore load finishes.

The sidecar now exposes that load as status "loading" on the local-prop row,
the cascade writes syncLocalLoading / syncNoTarget when it contacted no node
(never overwriting a real per-node error), and the auto-sync tick re-reads the
sidecar while no cascade candidate exists so Auto recovers on its own once an
announce lands or the local store finishes loading.
Quitting waited on a graceful Reticulum stack teardown before app:quit even
started: the renderer awaited reticulum.stop(), which ran the prepare-stop BLE
detach drain (1s cap) plus a 5s SIGTERM grace, adding roughly 2s to every exit.

Give quit a single owner. Main stops the sidecar with stop({ forQuit: true }),
skipping prepare-stop and SIGKILLing after 750ms, and escalates an in-flight
graceful stop instead of waiting on it. The connected Disconnect & Quit path no
longer calls onDisconnect, since main already tears down BLE, TCP, and the
sidecar. Stay-in-app Stop / Disconnect keeps the graceful drain.
Auto claimed to use discovered propagation nodes, but the sidecar built its
outbound cascade from persisted rows only, so the Chat "no propagation node"
banner was telling the truth and offline DMs had nowhere to go until the user
added a node by hand.

Auto now cascades onto heard PNs (cap 3, hop-sorted, nothing persisted) after
the nodes the user added and before the local inbox, and the announce handler
shares the rebuild helper so a newly heard PN is eligible without a restart.
The renderer counts discovered nodes in Auto to match, which hides the banner
and holds off the link-timeout failure bridge while the sidecar is still
cascading. The banner also gains a dismiss control backed by a Network toggle,
and every sync attempt records its target so progress, errors, and toasts name
the node they reached -- unattributed when the cascade contacted nobody.
Wait for each sync attempt to settle before trying the next node so a
discovered PN that accepts then times out no longer ends Auto/Manual
cascades before remotes or local-prop are tried.
Omit recently failed remotes, cap each cascade attempt at ~60s, and
refresh sidecar nodes before local fallback so Auto does not stall on
dead discovered PNs or skip an enabled host inbox.
Treat OUTBOUND_BUSY as a soft defer (no 15-minute backoff), keep remote
errors when local is still loading, fix stale modeHelpAuto/syncLocalLoading
locales, align docs, dedupe cascade helpers, and add i18n quality guards.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rinchen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 2b682a7f-a939-4211-ac58-a2317e3a7a20

📥 Commits

Reviewing files that changed from the base of the PR and between 752eac5 and 6d479dc.

📒 Files selected for processing (22)
  • docs/ci-cd.md
  • docs/reticulum.md
  • reticulum-sidecar/src/stack/mod.rs
  • scripts/check-i18n-quality.mjs
  • scripts/check-i18n-quality.test.mjs
  • scripts/clone-ratspeak-stack.sh
  • src/renderer/components/ReticulumPropagationNotice.test.tsx
  • src/renderer/components/ReticulumPropagationNotice.tsx
  • src/renderer/components/ReticulumPropagationSection.test.tsx
  • src/renderer/components/ReticulumPropagationSection.tsx
  • src/renderer/components/ReticulumPropagationSyncProgress.tsx
  • src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts
  • src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts
  • src/renderer/lib/reticulum/reticulumPropagationMode.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationMode.ts
  • src/renderer/lib/reticulum/reticulumPropagationSync.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationSync.ts
  • src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.ts
  • src/renderer/lib/reticulum/useReticulumPropagationAutoSync.ts
  • src/renderer/stores/reticulumPropagationStore.ts
📝 Walkthrough

Walkthrough

The PR adds persisted Off, Auto, and Manual propagation modes, discovered-node cascade synchronization, target-aware sync state, diagnostics, and quit-specific sidecar shutdown. It also moves Rust stack checkouts to .rsstack/, updates CI and scripts, removes the pre-push hook, and expands tests and documentation.

Changes

Reticulum propagation and workspace update

Layer / File(s) Summary
Repository-local Rust workspace
.github/workflows/*, scripts/*, reticulum-sidecar/Cargo.toml, README.md, docs/*, .gitignore, ignore configurations
Rust stack checkouts now use .rsstack/. CI, scripts, Cargo paths, tests, and documentation use the new layout.
Sidecar propagation modes and cascades
reticulum-sidecar/src/api/*, reticulum-sidecar/src/stack/*
The sidecar persists propagation modes, validates sync requests, tracks local loading, and builds mode-aware cascades with configured and discovered nodes.
Renderer propagation synchronization
src/renderer/lib/reticulum/*, src/renderer/stores/*, src/renderer/components/ReticulumPropagation*, src/renderer/runtime/*
The renderer adds mode-aware target selection, cascade settlement, cancellation, backoff, target reporting, persisted notice dismissal, and propagation diagnostics.
Shutdown and diagnostic handling
src/main/*, src/renderer/components/ConnectionPanel*, src/main/support-bundle*
Quit-time shutdown skips graceful BLE draining and escalates faster. Support bundles retain propagation island and outbound diagnostics.
Propagation localization validation
scripts/check-i18n-quality*
Locale checks detect stale propagation-mode help, incorrect loading terminology, and an invalid Czech Manual translation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReticulumPropagationSection
  participant reticulumPropagationStore
  participant reticulumPropagationAutoApply
  participant ReticulumSidecar
  User->>ReticulumPropagationSection: Select mode or start sync
  ReticulumPropagationSection->>reticulumPropagationStore: Set mode or sync target
  reticulumPropagationStore->>ReticulumSidecar: Send mode or destination-hash request
  ReticulumPropagationSection->>reticulumPropagationAutoApply: Start propagation cascade
  reticulumPropagationAutoApply->>ReticulumSidecar: Attempt discovered, configured, or local target
  ReticulumSidecar-->>reticulumPropagationAutoApply: Settlement or failure
  reticulumPropagationAutoApply-->>reticulumPropagationSection: Target-aware result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: propagation modes, multi-step sync, Auto-discovered nodes, and the .rsstack workspace layout.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reticulum-propagation-mode-network

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rinchen rinchen changed the title fix(reticulum): Network propagation mode and PN sync cascade fix(reticulum): Network PN mode, multi-step sync cascade, Auto discovered deposits, and .rsstack layout Aug 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
scripts/check-i18n-quality.mjs-1400-1408 (1)

1400-1408: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Detect non-Latin loading-status errors.

Require the pronoun check to split on punctuation instead of punctuation plus whitespace; no-whitespace clauses in Japanese and Chinese are currently skipped. Match sync terms without ASCII \b boundaries, and add regression coverage for Japanese/Chinese no-whitespace cases. Add or update behavioral tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-i18n-quality.mjs` around lines 1400 - 1408, Update the clause
splitting in the loading-status detection near mentionsLoading and
mentionsSyncOnly to split on sentence punctuation alone, preserving Japanese and
Chinese clauses without whitespace. Replace the ASCII word-boundary requirement
in the sync-term regex with Unicode-safe matching that detects non-Latin terms,
and add behavioral regression tests covering Japanese and Chinese no-whitespace
loading-status errors.

Source: Path instructions

src/renderer/components/ReticulumPropagationNotice.tsx-25-50 (1)

25-50: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make propagation mode reactive.

readReticulumPropagationMode() only reads localStorage during render. Selecting Off writes localStorage but does not update state observed by this component. The notice can remain visible until an unrelated render occurs.

Store the mode in reactive state, or subscribe through a mode hook. Add a transition test for selecting Off while the notice is visible.

As per coding guidelines, use Zustand persistence for localStorage-backed renderer state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/ReticulumPropagationNotice.tsx` around lines 25 - 50,
Replace the one-time read from readReticulumPropagationMode() in
ReticulumPropagationNotice with reactive Zustand-backed mode state, using the
existing persistence pattern for localStorage. Ensure selecting Off updates the
observed mode immediately so the notice unmounts without an unrelated render,
and add a transition test covering that interaction while the notice is visible.

Source: Coding guidelines

src/renderer/components/ReticulumPropagationSection.tsx-231-235 (1)

231-235: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gate the Auto kick on an available cascade candidate.

resolvePropagationSyncTargetId('auto', ...) returns null when no discovered node, no configured remote, and no enabled local node exists. The ?? 'local-prop' fallback then starts a cascade against a node that cannot serve. The user sees a failure toast only because the mode was switched. The bottom Sync button already uses hasPropagationCascadeCandidate for the same decision. Apply the same check here.

🐛 Proposed fix
     if (next !== 'auto') return;
     // Auto: kick discovered hash sync → configured → local (no Add, no Preferred).
-    const target =
-      resolvePropagationSyncTargetId('auto', nodes, preferredId, discovered) ?? 'local-prop';
-    handleSyncNow(target);
+    if (!hasPropagationCascadeCandidate('auto', nodes, discovered)) return;
+    const target =
+      resolvePropagationSyncTargetId('auto', nodes, preferredId, discovered) ?? 'local-prop';
+    handleSyncNow(target);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/ReticulumPropagationSection.tsx` around lines 231 -
235, Update the Auto branch near resolvePropagationSyncTargetId and
handleSyncNow to first require hasPropagationCascadeCandidate with the current
nodes, preferredId, and discovered state; return without starting
synchronization when no cascade candidate is available. Remove the ??
'local-prop' fallback so handleSyncNow is called only with a valid resolved
target.
src/renderer/lib/reticulum/reticulumPropagationMode.ts-216-231 (1)

216-231: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exclude a loading local node from the resolved sync target.

hasEnabledLocalPropagationNode only checks enabled. It ignores status: 'loading'. When the local inbox is enabled but the sidecar is still reading its messagestore, both Manual and Auto resolve to 'local-prop', and hasPropagationCascadeCandidate reports a candidate. The bottom Sync button in ReticulumPropagationSection.tsx then stays enabled and starts a cascade against a node that cannot settle.

The per-row Sync button already guards this case at ReticulumPropagationSection.tsx Line 447 with the comment "Local inbox cannot settle until its messagestore finishes loading". This module exports isLocalPropagationLoading for the same purpose but does not apply it here.

Apply the loading check in the local fallback of both branches, and in hasPropagationCascadeCandidate. Add a test that covers an enabled local node with status: 'loading' and no remotes; the existing test at ReticulumPropagationSection.test.tsx Line 525 uses enabled: false, so it does not reach this path.

🐛 Proposed fix
 export function hasEnabledLocalPropagationNode(nodes: PropagationNodeRow[]): boolean {
   return nodes.some((n) => n.id === 'local-prop' && n.enabled);
 }
+
+/** Local inbox that is enabled and has finished loading its messagestore. */
+export function hasSyncableLocalPropagationNode(nodes: PropagationNodeRow[]): boolean {
+  return nodes.some((n) => n.id === 'local-prop' && n.enabled && n.status !== 'loading');
+}
   if (mode === 'manual') {
     if (preferredId != null && preferredId.length > 0) return preferredId;
     const configuredBest = listConfiguredRemotePropagationIds(nodes).at(0);
     if (configuredBest != null) return configuredBest;
-    return hasEnabledLocalPropagationNode(nodes) ? 'local-prop' : null;
+    return hasSyncableLocalPropagationNode(nodes) ? 'local-prop' : null;
   }
   const discoveredBest = listDiscoveredPropagationTargets(nodes, discovered).at(0);
   if (discoveredBest != null) {
     return discoveredBest.destinationHash.toLowerCase();
   }
   const configured = listConfiguredRemotePropagationIds(nodes).at(0);
   if (configured != null) return configured;
-  if (hasEnabledLocalPropagationNode(nodes)) return 'local-prop';
+  if (hasSyncableLocalPropagationNode(nodes)) return 'local-prop';
   return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/reticulum/reticulumPropagationMode.ts` around lines 216 -
231, Update the local fallback logic in the propagation target resolver’s manual
and automatic branches to exclude nodes for which isLocalPropagationLoading
indicates loading, while preserving remote and ready-local selection. Apply the
same loading exclusion in hasPropagationCascadeCandidate so it cannot report a
loading local node as a candidate, and add coverage for an enabled loading local
node with no remotes.
🧹 Nitpick comments (3)
docs/ci-cd.md (1)

79-79: 🔒 Security & Privacy | 🔵 Trivial

Record immutable upstream revisions for release builds.

This documents floating all five dependencies to origin/main. The release-sidecar helper in scripts/build-reticulum-sidecar-release.mjs also invokes scripts/clone-ratspeak-stack.sh. Rebuilding the same release tag can therefore use different Rust sources, and an upstream change can alter or block a release.

Keep the floating default for development if required. Pin release refs or record resolved commit SHAs in CI artifacts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ci-cd.md` at line 79, Update the release build flow around
build-reticulum-sidecar-release.mjs and clone-ratspeak-stack.sh so release
builds use immutable refs or record the resolved commit SHAs for all five Rust
dependencies. Preserve the existing origin/main floating behavior for
development builds and ensure release artifacts retain the exact source
revisions used.
src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts (1)

540-553: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the Date.now spy in a teardown hook.

nowSpy.mockRestore() runs only after the assertions. If either expect on Lines 550-551 fails, the spy stays installed. Date.now then returns the frozen nowMs for every later test in this file, and the backoff window checks in reticulumPropagationSyncBackoff stop advancing. Move the restore into afterEach (or use vi.restoreAllMocks() there) so a failing assertion cannot leak the mock.

♻️ Proposed teardown change
   afterEach(() => {
     vi.unstubAllGlobals();
+    vi.restoreAllMocks();
   });
       await expect(startPropagationSyncCascade({ hasEnabledInterfaces: true })).resolves.toBe(true);
       expect(slowStartSync.mock.calls.map((c) => c[0])).toEqual([near, 'local-prop']);
-      nowSpy.mockRestore();
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts` around
lines 540 - 553, Move Date.now cleanup for the test in the “settles the local
inbox once the remote budget is spent” case into an afterEach teardown, using
nowSpy.mockRestore() or vi.restoreAllMocks(), and remove the assertion-dependent
restoration so failures cannot leak the mock.
src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.ts (1)

10-39: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Sweep expired entries when the failures map grows.

failures only drops a key when that same key is queried again after the backoff window. A target that fails once and then stops announcing is never re-queried, so its entry stays for the whole session. On a mesh with many announced propagation nodes the map grows without an upper bound.

Add a size-triggered sweep in noteReticulumPropagationSyncFailure.

♻️ Proposed sweep
 /** Sync target id (row id, `local-prop`, or destination hash) to last failure time. */
 const failures = new Map<string, number>();
+/** Sweep threshold — entries for targets that never return are otherwise never re-queried. */
+const FAILURES_SWEEP_AT = 256;
 
 function backoffKey(id: string): string {
   return id.toLowerCase();
 }
 
 export function noteReticulumPropagationSyncFailure(id: string, atMs = Date.now()): void {
   if (id.length === 0) return;
+  if (failures.size >= FAILURES_SWEEP_AT) {
+    for (const [key, at] of failures) {
+      if (atMs - at >= RETICULUM_PROPAGATION_SYNC_FAILURE_BACKOFF_MS) failures.delete(key);
+    }
+  }
   failures.set(backoffKey(id), atMs);
 }

As per coding guidelines: "Avoid hot-path O(n) work and perform lazy cleanup when collections become large."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.ts` around lines
10 - 39, Add a size-triggered lazy cleanup in
noteReticulumPropagationSyncFailure: when failures reaches the configured
large-map threshold, iterate entries and remove those whose failure timestamps
are outside RETICULUM_PROPAGATION_SYNC_FAILURE_BACKOFF_MS relative to the
current timestamp. Keep normal failure recording unchanged and avoid sweeping on
every call.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reticulum.md`:
- Line 423: Update the workspace-free stub-build statement in the documented
build description to clarify that Cargo still requires the .rsstack/ checkouts,
matching reticulum-sidecar/README.md and the manifest path dependencies.
Preserve the distinction between the stub stack and the real mesh-I/O stack.

In `@reticulum-sidecar/Cargo.toml`:
- Around line 66-129: Ensure the full-build flow keeps the Reticulum-sidecar
path dependencies rooted at the repository-local ../.rsstack checkout that
scripts/clone-ratspeak-stack.sh updates. Either materialize custom
WORKSPACE_ROOT contents at that location before invoking rns-stack, or
consistently propagate WORKSPACE_ROOT into every path dependency in the
Cargo.toml dependency declarations, including rns-runtime, rns-identity,
rns-wire, rns-ratkey, rns-transport, lxmf-core, nomad-core, lxst-telephony,
lxst-core, lrgp, rns-interface, rns-link, and rns-crypto.

In `@reticulum-sidecar/src/stack/mod.rs`:
- Around line 1399-1413: Update set_propagation_mode to snapshot the existing
propagation mode before calling inner.set_propagation_mode(mode), then restore
that prior mode when inner.save(&self.config_dir, &self.storage_dir) fails.
Follow the snapshot-and-restore pattern used by set_pn_hosting_policy and
set_path_medium_preference, while preserving the current successful-save
behavior.

In `@reticulum-sidecar/src/stack/persistence.rs`:
- Around line 409-411: Update StackHandle::set_propagation_mode to snapshot the
previous propagation mode before mutation and restore it when inner.save fails,
keeping PersistedState consistent with the live bridge on error. Add a test
covering save failure and asserting the prior mode is retained.

In `@src/renderer/components/ReticulumPropagationSection.tsx`:
- Around line 226-230: Both sidecar-update promise chains lack rejection
handling, allowing unhandled promise rejections. In
src/renderer/components/ReticulumPropagationSection.tsx lines 226-230, append a
catch to the setModeOnSidecar(next) chain that warns with the rejection error;
do the same in lines 565-572 for setAutoSyncIntervalOnSidecar(sec), using the
specified contextual warning messages and unknown error type.

In `@src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts`:
- Around line 116-125: Update tryLocalSettleIfEnabled so refreshFromSidecar is
wrapped in error handling: log the refresh failure, retain the nodes already
read from the store, and continue evaluating hasEnabledLocalPropagationNode
instead of rejecting. Preserve the existing refresh-and-recheck behavior when
the call succeeds.

In `@src/renderer/lib/reticulum/reticulumPropagationEffective.test.ts`:
- Around line 71-82: Update auto-discovery candidate handling in
hasEffectiveReticulumPropagationTarget and its candidate-building flow so
disabled configured propagation rows still reserve their destination_hash in the
discovered-node seen set, even though they are excluded from effective
configured candidates. Ensure matching announces are not appended as discovered
cascade targets, preserving the false result for disabled-only configurations.

In `@src/renderer/lib/reticulum/reticulumPropagationSync.ts`:
- Around line 298-306: Preserve supersession as a terminal non-success outcome
in the state update within the payload.active === false branch, using the
existing supersession marker rather than clearing lastSyncError to null. Update
awaitPropagationSyncSettled() to classify that marker as cancelled or
superseded, and add coverage that awaits a sync, applies
PROPAGATION_SYNC_SUPERSEDED, and verifies it does not resolve as success.

In `@src/renderer/lib/reticulum/useReticulumPropagationAutoSync.ts`:
- Around line 76-79: Attach rejection handlers to both mount-time calls in the
initialization flow: update refreshFromSidecar and setModeOnSidecar to catch
failures and log them with console.warn, while preserving their existing
fire-and-forget behavior. Do not alter the handled tick path through runTick.

---

Other comments:
In `@scripts/check-i18n-quality.mjs`:
- Around line 1400-1408: Update the clause splitting in the loading-status
detection near mentionsLoading and mentionsSyncOnly to split on sentence
punctuation alone, preserving Japanese and Chinese clauses without whitespace.
Replace the ASCII word-boundary requirement in the sync-term regex with
Unicode-safe matching that detects non-Latin terms, and add behavioral
regression tests covering Japanese and Chinese no-whitespace loading-status
errors.

In `@src/renderer/components/ReticulumPropagationNotice.tsx`:
- Around line 25-50: Replace the one-time read from
readReticulumPropagationMode() in ReticulumPropagationNotice with reactive
Zustand-backed mode state, using the existing persistence pattern for
localStorage. Ensure selecting Off updates the observed mode immediately so the
notice unmounts without an unrelated render, and add a transition test covering
that interaction while the notice is visible.

In `@src/renderer/components/ReticulumPropagationSection.tsx`:
- Around line 231-235: Update the Auto branch near
resolvePropagationSyncTargetId and handleSyncNow to first require
hasPropagationCascadeCandidate with the current nodes, preferredId, and
discovered state; return without starting synchronization when no cascade
candidate is available. Remove the ?? 'local-prop' fallback so handleSyncNow is
called only with a valid resolved target.

In `@src/renderer/lib/reticulum/reticulumPropagationMode.ts`:
- Around line 216-231: Update the local fallback logic in the propagation target
resolver’s manual and automatic branches to exclude nodes for which
isLocalPropagationLoading indicates loading, while preserving remote and
ready-local selection. Apply the same loading exclusion in
hasPropagationCascadeCandidate so it cannot report a loading local node as a
candidate, and add coverage for an enabled loading local node with no remotes.

---

Nitpick comments:
In `@docs/ci-cd.md`:
- Line 79: Update the release build flow around
build-reticulum-sidecar-release.mjs and clone-ratspeak-stack.sh so release
builds use immutable refs or record the resolved commit SHAs for all five Rust
dependencies. Preserve the existing origin/main floating behavior for
development builds and ensure release artifacts retain the exact source
revisions used.

In `@src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts`:
- Around line 540-553: Move Date.now cleanup for the test in the “settles the
local inbox once the remote budget is spent” case into an afterEach teardown,
using nowSpy.mockRestore() or vi.restoreAllMocks(), and remove the
assertion-dependent restoration so failures cannot leak the mock.

In `@src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.ts`:
- Around line 10-39: Add a size-triggered lazy cleanup in
noteReticulumPropagationSyncFailure: when failures reaches the configured
large-map threshold, iterate entries and remove those whose failure timestamps
are outside RETICULUM_PROPAGATION_SYNC_FAILURE_BACKOFF_MS relative to the
current timestamp. Keep normal failure recording unchanged and avoid sweeping on
every call.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 07cf470f-c83c-4940-9483-9a9a7d3e9d7b

📥 Commits

Reviewing files that changed from the base of the PR and between e5b09d4 and 752eac5.

⛔ Files ignored due to path filters (18)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • reticulum-sidecar/patches/README.md is excluded by !reticulum-sidecar/patches/**
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (96)
  • .githooks/pre-push
  • .github/workflows/build.yaml
  • .github/workflows/flatpak.yaml
  • .github/workflows/release.yaml
  • .github/workflows/reticulum-sidecar.yaml
  • .github/workflows/tests.yaml
  • .gitignore
  • .markdownlint-cli2.jsonc
  • .prettierignore
  • .yamllint
  • AGENTS.md
  • README.md
  • docs/ci-cd.md
  • docs/development-environment.md
  • docs/nomad-hosting-interop.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • reticulum-sidecar/Cargo.toml
  • reticulum-sidecar/README.md
  • reticulum-sidecar/src/api/mod.rs
  • reticulum-sidecar/src/api/propagation.rs
  • reticulum-sidecar/src/stack/live.rs
  • reticulum-sidecar/src/stack/lxmf_outbound.rs
  • reticulum-sidecar/src/stack/mod.rs
  • reticulum-sidecar/src/stack/persistence.rs
  • reticulum-sidecar/src/stack/pn_cascade.rs
  • reticulum-sidecar/src/stack/propagation_bridge.rs
  • reticulum-sidecar/src/stack/propagation_mode.rs
  • scripts/apply-rsLXMF-link-delivery-has-pending-to.sh
  • scripts/apply-rsLXMF-propagation-node-deferred-messagestore-load.sh
  • scripts/apply-rsLXMF-propagation-node-policy-setters.sh
  • scripts/apply-rsLXMF-propagation-sync-peering.sh
  • scripts/apply-rsReticulum-auto-beacon-utun.sh
  • scripts/apply-rsReticulum-ble-rnode-bond-desync.sh
  • scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh
  • scripts/apply-rsReticulum-discovery-announce-egress.sh
  • scripts/apply-rsReticulum-inbound-raw-saturation-log.sh
  • scripts/apply-rsReticulum-link-client-nomad.sh
  • scripts/apply-rsReticulum-link-client-proof-budget.sh
  • scripts/apply-rsReticulum-packet-tap.sh
  • scripts/apply-rsReticulum-path-medium-slots.sh
  • scripts/build-reticulum-sidecar-release.mjs
  • scripts/check-i18n-quality.mjs
  • scripts/check-i18n-quality.test.mjs
  • scripts/check-reticulum-sidecar.sh
  • scripts/check-rsnomad-fmt.sh
  • scripts/check-rsnomad.sh
  • scripts/clone-ratspeak-stack.sh
  • scripts/clone-ratspeak-stack.test.mjs
  • scripts/ensure-rsReticulum-patches.sh
  • scripts/update.sh
  • scripts/update.test.mjs
  • src/main/index.ts
  • src/main/reticulum-sidecar-manager.test.ts
  • src/main/reticulum-sidecar-manager.ts
  • src/main/reticulum-sidecar-path.test.ts
  • src/main/reticulum-sidecar-path.ts
  • src/main/support-bundle.test.ts
  • src/main/support-bundle.ts
  • src/renderer/App.tsx
  • src/renderer/components/AppPanel.tsx
  • src/renderer/components/ConnectionPanel.test.tsx
  • src/renderer/components/ConnectionPanel.tsx
  • src/renderer/components/ReticulumAppPanelSection.tsx
  • src/renderer/components/ReticulumPropagationControls.test.tsx
  • src/renderer/components/ReticulumPropagationControls.tsx
  • src/renderer/components/ReticulumPropagationNotice.test.tsx
  • src/renderer/components/ReticulumPropagationNotice.tsx
  • src/renderer/components/ReticulumPropagationSection.test.tsx
  • src/renderer/components/ReticulumPropagationSection.tsx
  • src/renderer/components/ReticulumPropagationSyncProgress.test.tsx
  • src/renderer/components/ReticulumPropagationSyncProgress.tsx
  • src/renderer/lib/reticulum/reticulumDiagnosticSnapshot.test.ts
  • src/renderer/lib/reticulum/reticulumDiagnosticSnapshot.ts
  • src/renderer/lib/reticulum/reticulumDiscoveryAnnounceEgress.contract.test.ts
  • src/renderer/lib/reticulum/reticulumLinkDeliveryHasPendingTo.contract.test.ts
  • src/renderer/lib/reticulum/reticulumLxmfOutboundBackchannel.contract.test.ts
  • src/renderer/lib/reticulum/reticulumOutboundFailureBridge.test.ts
  • src/renderer/lib/reticulum/reticulumOutboundFailureBridge.ts
  • src/renderer/lib/reticulum/reticulumPnHostingPolicySetters.contract.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts
  • src/renderer/lib/reticulum/reticulumPropagationEffective.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationEffective.ts
  • src/renderer/lib/reticulum/reticulumPropagationMode.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationMode.ts
  • src/renderer/lib/reticulum/reticulumPropagationSync.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationSync.ts
  • src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.test.ts
  • src/renderer/lib/reticulum/reticulumPropagationSyncBackoff.ts
  • src/renderer/lib/reticulum/useReticulumPropagationAutoSync.test.ts
  • src/renderer/lib/reticulum/useReticulumPropagationAutoSync.ts
  • src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/stores/reticulumPropagationStore.test.ts
  • src/renderer/stores/reticulumPropagationStore.ts
💤 Files with no reviewable changes (6)
  • .githooks/pre-push
  • src/renderer/components/ReticulumPropagationControls.tsx
  • src/renderer/components/ReticulumAppPanelSection.tsx
  • src/renderer/components/ReticulumPropagationControls.test.tsx
  • src/renderer/App.tsx
  • src/renderer/components/AppPanel.tsx

Comment thread docs/reticulum.md Outdated
Comment on lines +66 to +129
path = "../.rsstack/rsReticulum/crates/rns-runtime"
optional = true
features = ["serial"]

[dependencies.rns-identity]
package = "rns-identity"
path = "../../rsReticulum/crates/rns-identity"
path = "../.rsstack/rsReticulum/crates/rns-identity"
optional = true

[dependencies.rns-wire]
package = "rns-wire"
path = "../../rsReticulum/crates/rns-wire"
path = "../.rsstack/rsReticulum/crates/rns-wire"
optional = true
features = ["std"]

[dependencies.rns-ratkey]
package = "rns-ratkey"
path = "../../rsReticulum/crates/rns-ratkey"
path = "../.rsstack/rsReticulum/crates/rns-ratkey"
optional = true

[dependencies.rns-transport]
package = "rns-transport"
path = "../../rsReticulum/crates/rns-transport"
path = "../.rsstack/rsReticulum/crates/rns-transport"
optional = true

[dependencies.lxmf-core]
package = "lxmf-core"
path = "../../rsLXMF/crates/lxmf-core"
path = "../.rsstack/rsLXMF/crates/lxmf-core"
optional = true

[dependencies.nomad-core]
package = "nomad-core"
path = "../../rsNomad/crates/nomad-core"
path = "../.rsstack/rsNomad/crates/nomad-core"
optional = true

[dependencies.lxst-telephony]
package = "lxst-telephony"
path = "../../rsLXST/crates/lxst-telephony"
path = "../.rsstack/rsLXST/crates/lxst-telephony"
optional = true

[dependencies.lxst-core]
package = "lxst-core"
path = "../../rsLXST/crates/lxst-core"
path = "../.rsstack/rsLXST/crates/lxst-core"
optional = true

[dependencies.lrgp]
package = "lrgp"
path = "../../lrgp-rs"
path = "../.rsstack/lrgp-rs"
optional = true

[dependencies.rns-interface]
package = "rns-interface"
path = "../../rsReticulum/crates/rns-interface"
path = "../.rsstack/rsReticulum/crates/rns-interface"
optional = true
features = ["ble"]

[dependencies.rns-link]
package = "rns-link"
path = "../../rsReticulum/crates/rns-link"
path = "../.rsstack/rsReticulum/crates/rns-link"
optional = true

[dependencies.rns-crypto]
package = "rns-crypto"
path = "../../rsReticulum/crates/rns-crypto"
path = "../.rsstack/rsReticulum/crates/rns-crypto"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^scripts/clone-ratspeak-stack\.sh$|Cargo\.toml$|AGENTS\.md$|.*\.sh$)' | sed -n '1,200p'

echo "== clone-ratspeak-stack outline/search =="
if [ -f scripts/clone-ratspeak-stack.sh ]; then
  wc -l scripts/clone-ratspeak-stack.sh
  sed -n '1,260p' scripts/clone-ratspeak-stack.sh
else
  fd -a 'clone-ratspeak-stack\.sh$' .
fi

echo "== relevant RS_*_DIR / .rsstack references =="
rg -n "WORKSPACE_ROOT|RS_(RETICULUM|LXMF|NOMAD|LXST)_DIR|rns-|workspace|full-build|build\.rs|\.rsstack|cargo" -S . \
  --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!target' | sed -n '1,260p'

echo "== Cargo.toml context =="
if [ -f reticulum-sidecar/Cargo.toml ]; then
  sed -n '50,145p' reticulum-sidecar/Cargo.toml
fi

Repository: Colorado-Mesh/mesh-client

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== scripts/apply-ratspeak-overlay-apply-list.sh =="
sed -n '1,180p' scripts/lib/apply-ratspeak-overlay-apply-list.sh

echo "== overlay scripts RS dir defaults =="
for f in scripts/apply-rsReticulum-*.sh scripts/apply-rsLXMF-*.sh; do
  echo "-- $f"
  sed -n '1,25p' "$f"
done

echo "== Cargo path references in reticulum-sidecar =="
rg -n "path\s*=|features\s*=|rns-stack|rns-ble|rns-rnode-tcp" reticulum-sidecar/Cargo.toml reticulum-sidecar/rust-toolchain.toml

Repository: Colorado-Mesh/mesh-client

Length of output: 306


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate lib =="
fd -a 'apply-ratspeak-overlay-apply-list\.sh$|apply-ratspeak-overlay.*\.sh$' .

echo "== lib content if present =="
if [ -f scripts/lib/apply-ratspeak-overlay-apply-list.sh ]; then
  wc -l scripts/lib/apply-ratspeak-overlay-apply-list.sh
  sed -n '1,220p' scripts/lib/apply-ratspeak-overlay-apply-list.sh
else
  fd -a 'apply-ratspeak-overlay-apply-list\.sh$' . | sed -n '1,50p'
fi

echo "== overlay scripts default values =="
for f in $(fd -a 'apply-rsReticulum.*\.sh$|apply-rsLXMF.*\.sh$' scripts | sort); do
  printf '\n-- %s\n' "$f"
  sed -n '1,28p' "$f"
done

echo "== clone script relevant default/export refs =="
python3 - <<'PY'
from pathlib import Path
p=Path('scripts/clone-ratspeak-stack.sh')
lines=p.read_text().splitlines()
for i,l in enumerate(lines,1):
    if 'WORKSPACE_ROOT=' in l or 'RS_RETICULUM_DIR' in l or 'RS_LXMF_DIR' in l or 'source "${SCRIPT_DIR}/lib/ratspeak-overlay-apply-list.sh"' in l:
        print(f'{i}: {l}')
PY

echo "== Cargo workspace/path comment =="
sed -n '1,45p' reticulum-sidecar/Cargo.toml
sed -n '60,132p' reticulum-sidecar/Cargo.toml

Repository: Colorado-Mesh/mesh-client

Length of output: 18845


Keep Cargo paths fixed to the repo-local .rsstack/ workspace.

scripts/clone-ratspeak-stack.sh accepts WORKSPACE_ROOT outside the repository, but every Reticulum-sidecar path dependency is relative to ../.rsstack. If a user runs the clone script with a custom root and then builds the sidecar with rns-stack, Cargo may source patchable files from a different checkout than the script updated. Make the full-build flow materialize at ../.rsstack, or update every path dependency to honor the injected root using an environment variable/resolved path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reticulum-sidecar/Cargo.toml` around lines 66 - 129, Ensure the full-build
flow keeps the Reticulum-sidecar path dependencies rooted at the
repository-local ../.rsstack checkout that scripts/clone-ratspeak-stack.sh
updates. Either materialize custom WORKSPACE_ROOT contents at that location
before invoking rns-stack, or consistently propagate WORKSPACE_ROOT into every
path dependency in the Cargo.toml dependency declarations, including
rns-runtime, rns-identity, rns-wire, rns-ratkey, rns-transport, lxmf-core,
nomad-core, lxst-telephony, lxst-core, lrgp, rns-interface, rns-link, and
rns-crypto.

Source: Path instructions

Comment thread reticulum-sidecar/src/stack/mod.rs
Comment on lines +409 to +411
pub fn set_propagation_mode(&mut self, mode: PropagationMode) {
self.propagation_mode = mode;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Restore the prior mode if persistence fails.

StackHandle::set_propagation_mode calls this mutator before inner.save(...). If the save fails, PersistedState retains the new mode, but the live bridge keeps the old mode because the method returns early. Subsequent reads report behavior that the live bridge does not use.

Snapshot and restore the prior state when the save fails. Add a save-failure test.

As per coding guidelines, “Stateful or I/O code must preserve integrity on failure.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reticulum-sidecar/src/stack/persistence.rs` around lines 409 - 411, Update
StackHandle::set_propagation_mode to snapshot the previous propagation mode
before mutation and restore it when inner.save fails, keeping PersistedState
consistent with the live bridge on error. Add a test covering save failure and
asserting the prior mode is retained.

Source: Coding guidelines

Comment thread src/renderer/components/ReticulumPropagationSection.tsx Outdated
Comment thread src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts
Comment on lines +71 to +82
it('returns false in auto when the discovered node is already added but disabled', () => {
const disabledConfigured: PropagationNodeRow = {
...remoteNode,
enabled: false,
destination_hash: activeDiscovered.destination_hash,
};
expect(
hasEffectiveReticulumPropagationTarget([disabledConfigured], null, 'auto', [
activeDiscovered,
]),
).toBe(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare renderer and sidecar handling of discovered nodes whose configured row is disabled.
set -euo pipefail

# Renderer filter: does it exclude configured hashes regardless of `enabled`?
rg -nP --type=ts -C15 'function listDiscoveredPropagationTargets'

# Sidecar: seed of the dedup set and the disabled-row skip.
rg -nP --type=rust -C10 'fn auto_discovered_candidates|fn candidates_from_propagation_rows' \
  reticulum-sidecar/src/stack/pn_cascade.rs

Repository: Colorado-Mesh/mesh-client

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching reticulumPropagationEffective:"
fd -a 'reticulumPropagationEffective' . || true

echo "Files matching pn_cascade.rs:"
fd -a 'pn_cascade\.rs' . || true

echo "Search listDiscoveredPropagationTargets / hasEffectiveReticulumPropagationTarget:"
rg -n --type=ts --type=t 'listDiscoveredPropagationTargets|hasEffectiveReticulumPropagationTarget|candidates_from_propagation_rows|auto_discovered_candidates' . || true

echo "Search node enabled handling in renderer:"
rg -n --type=ts --type=t 'destination_hash|enabled|discovered' src/renderer -C 3 || true

echo "Search disabled propagation row handling in sidecar:"
rg -n --type=rust 'enabled|destination_hash|seen|auto_discovered|candidates_from_propagation_rows' reticulum-sidecar/src/stack/pn_cascade.rs -C 8 || true

echo "Diff stat:"
git diff --stat || true

Repository: Colorado-Mesh/mesh-client

Length of output: 12996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Renderer effective files:"
cat -n src/renderer/lib/reticulum/reticulumPropagationEffective.ts
echo "---- test ----"
cat -n src/renderer/lib/reticulum/reticulumPropagationEffective.test.ts | sed -n '1,140p'

echo "Sidecar relevant impl:"
cat -n reticulum-sidecar/src/stack/pn_cascade.rs | sed -n '118,202p'

echo "Call sites for auto_discovered_candidates:"
rg -n --type=rust 'auto_discovered_candidates|candidates_for_propagation_mode|candidates_from_propagation_rows' . -C 5 || true

echo "Search call sites for hasEffectiveReticulumPropagationTarget:"
rg -n --type=ts --type=tsx 'hasEffectiveReticulumPropagationTarget|listDiscoveredPropagationTargets' src -C 8 || true

echo "Behavioural probe sidecar dedup semantics (read-only Rust text analysis):"
python3 - <<'PY'
from pathlib import Path
p = Path('reticulum-sidecar/src/stack/pn_cascade.rs')
s = p.read_text()
start = s.index('pub fn auto_discovered_candidates')
end = s.index('\n}\n', start) + 4
print(s[start:end])
PY

Repository: Colorado-Mesh/mesh-client

Length of output: 29650


Align auto_discovered_candidates with disabled configured rows.

In Auto mode, candidates_from_propagation_rows skips rows where enabled == false, then auto_discovered_candidates initializes its seen set from the remaining candidates. A discovered announce matching a disabled configured hash is not seen, so it can be appended as a discovered cascade target and receive a deposit even though the renderer reports no effective target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/reticulum/reticulumPropagationEffective.test.ts` around
lines 71 - 82, Update auto-discovery candidate handling in
hasEffectiveReticulumPropagationTarget and its candidate-building flow so
disabled configured propagation rows still reserve their destination_hash in the
discovered-node seen set, even though they are excluded from effective
configured candidates. Ensure matching announces are not appended as discovered
cascade targets, preserving the false result for disabled-only configurations.

Comment thread src/renderer/lib/reticulum/reticulumPropagationSync.ts
Comment thread src/renderer/lib/reticulum/useReticulumPropagationAutoSync.ts Outdated
Roll back propagation mode on sidecar save failure, keep supersede as a
non-success settle marker, make Chat notice mode reactive, and harden
cascade/local-loading/i18n edge cases from PR review.
@rinchen
rinchen merged commit 948e6bc into main Aug 8, 2026
21 checks passed
@rinchen
rinchen deleted the fix/reticulum-propagation-mode-network branch August 8, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant