Skip to content

fix: Graph hop filters, RRC /who flood, MeshCore auto-login stampede, and related races - #839

Merged
rinchen merged 9 commits into
mainfrom
fixes
Aug 11, 2026
Merged

rinchen merged 9 commits into
mainfrom
fixes

Conversation

@rinchen

@rinchen rinchen commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

This branch is a batch of user-facing and ops fixes across Graph/Topology, RRC, MeshCore Rooms, Meshtastic TCP, and pnpm run update. Nothing here is a new product feature; each change closes a race, a misleading filter, or noisy false-positive logging.

8 commits (origin/main..HEAD):

  1. 3ffef32b — MeshCore room auto-login stampede + waiting-message drain timeouts + expected reconnect log noise
  2. 96f8287d — RRC hidden /who flooding the room transcript
  3. 11e6f38a — Graph/Topology 48-node layout budget hiding nodes despite hop filters; Topology RF only
  4. 4cdc3587 — dependency bumps
  5. c426f8ffpnpm run update only warns on newer published Ratspeak releases
  6. cb5299d8 — review follow-ups: TCP write sentinel, auto-login across disconnect, exact RF matching, /who roster merge
  7. 7b542766 — Graph Max hops was a no-op when Show distant was off
  8. d712adc9 — unknown hops were treated as 1-hop neighbors; layout budget is 400 after hop filters

Graph / Topology hop filters and visible-node cap

Problem: Meshtastic/MeshCore Graph and Reticulum Topology could hide real nearby peers (or ignore the hop controls) for three independent reasons:

  • A 48-node layout budget ran after hop filters and could drop 1-hop neighbors when Show distant was on (MQTT/RNS maps).
  • Max hops was ANDed with the nearby hop ceiling, so Max hops = 2 or 8 did nothing while Show distant was off (Graph default).
  • Peers with unknown hops (hops_away / hops null) were treated as 1-hop neighbors, so they passed numeric Max hops and the nearby ceiling.

Fix (topologyGraphLimits.ts, shared by both panels):

  • Numeric Max hops always applies.
  • Unknown hops are included only when Max hops is All hops and Show distant is on — they are not 1-hop neighbors.
  • The nearby hop ceiling (Mesh hops > 1, Reticulum hops > 2, because RNS hop counts are 1-based for a direct path) applies only when Max hops is All.
  • Drawn-node layout budget after hop filters is 400 (FORCE_REPULSION_FULL_PAIR_CAP). The old 48 cap no longer hides real 1-hop peers behind Show distant.
  • Toolbar always states the 400-node limit; when the cap hides nodes the status reads “Showing N of M nodes (limit 400)”.

Reticulum Topology extras:

  • New RF only checkbox: keep RNode / KISS / LoRa / BLE RNode / BLE Peer spokes; drop TCP / I2P / Auto hubs and their peers.
  • RF matching is exact name or id (no substring) so "RNode" does not keep "RNode_TCP_East".
  • RF-only runs before the 800 last-seen path-table ingest slice so a TCP-heavy table cannot starve RF peers.
  • Filter changes rebuild from a cached fetch (no extra sidecar round-trip). serial_port is passed through so BLE RNode classification is accurate.

Docs: README known-issues, docs/diagnostics.md, docs/troubleshooting.md, docs/reticulum.md. i18n keys: peerGraph.hiddenCountLimit, peerGraph.visibleNodeLimitNote, reticulumTopology.hiddenCountLimit, reticulumTopology.visibleNodeLimitNote, reticulumTopology.rfOnly.


RRC: stop hidden /who from flooding chat

Problem: Auto /who (needed because rrcd JOINED member lists are optional) was gated by a panel useRef. Leaving RRC and coming back remounted the panel, cleared the gate, and polled /who again. Every snapshot dumped into the focused room as NOTICE text. Unscoped hub notices (empty K_ROOM) also landed in whatever room was focused.

Fix:

  • Once-per-join /who gate lives on the hub session (whoRequestedRooms), so it survives panel remount. Cleared on part / hub teardown; failed sends release the slot.
  • First /who snapshot may appear in the named room; later snapshots update the nicklist only (whoTranscriptShownRooms). Refresh / composer /who can force one more transcript line (reserveWhoTranscriptForce).
  • Hidden /who is a hub-global slash command: omit K_ROOM, send /who <token> only (token rejects whitespace / extra slash).
  • Notices with empty K_ROOM go to [hub], never the focused chat room.
  • /who NOTICE room is resolved against joined rooms only (rrcWhoNoticeJoinedRoom) so a partial nicklist cannot skip a later full roster, and an unknown/evil room name is ignored.

MeshCore: room auto-login stampede and waiting-message drain

Problem: Connect auto-login re-ran pathSync on every node-list change (unrelated advert churn). Overlapping passes raced the companion RF lane. Disconnect did not invalidate an in-flight pass, so a dying login could SendLogin on the new connection. Silent bulk getWaitingMessages always waited the full 45s timeout before falling back, even after consecutive failures.

Fix:

  • Single-flight auto-login (runMeshcoreRoomAutoLoginSingleFlight): overlapping triggers join the in-flight pass and re-select targets when it finishes (a Room that appeared mid-pathSync still logs in).
  • Effect depends on a stable ready key (configured Room contacts + pubkey hydration), not nodes.size, with a 500ms debounce.
  • Disconnect bumps a generation; abortIfStale after pathSync prevents SendLogin on the new conn. The promise is left in place so reconnect joins it instead of overlapping.
  • Targets skip already-queued / logged-in / failed rooms (selectMeshcoreRoomAutoLoginTargets).
  • Silent bulk getWaitingMessages circuit-opens after 2 consecutive timeouts and skips bulk until reconnect/success. Disconnect bumps the attempt id so a late timeout cannot trip the next connection.

Meshtastic TCP write + expected log noise

Problem: meshtastic:tcp-write with no socket rejected in main, so Electron logged handler [error] on every reconnect race. If that rejection were swallowed, frames could be silently dropped. Chromium ResizeObserver loop warnings were forwarded as renderer errors.

Fix:

  • Main resolves the sentinel 'no-socket' (debug log only) so Electron does not log handler [error].
  • Preload maps that sentinel to throw new Error('meshtastic:tcp-write: no active socket') so TransportTcpIpc / the SDK see a failed write and do not replay bytes on a later socket.
  • Fast path for TCP loss remains onDisconnected (the no-socket message still does not match TRANSPORT_LOST_MESSAGE).
  • isDroppableRendererConsoleNoise drops ResizeObserver loop completed/limit-exceeded lines (including [violation] prefix).

pnpm run update: Ratspeak published-release pins

Problem: check_ratspeak_upstream treated every latest GitHub Release as news. Already-reviewed tags (Ratspeak v1.0.25) and repos without Releases (LXMFace) looked like errors. Tags / main / RCs without a published Release were noisy.

Fix:

  • Each watch entry has a reviewed-ref pin (v0.1.2, v1.0.25, empty, or file:<path>@<sha> for vendored files).
  • Warn only when a published GitHub Release (or vendored file commit) differs from the pin.
  • games-parity still points at Games tab review docs; additionally flags Four in a Row when the release body or compare diff mentions it.
  • LXMFace is tracked as file:js/lxmface.js@308a729d… against src/renderer/lib/reticulum/lxmface.ts.

Dependency bumps (4cdc3587)

  • @zip.js/zip.js ^2.8.36^2.8.37
  • @typescript-eslint/eslint-plugin / parser / typescript-eslint ^8.66.0^8.67.0

Commits (origin/main..HEAD)

3ffef32b fix: stop room auto-login stampede and quiet expected reconnect log noise
96f8287d fix(rrc): stop hidden /who from flooding the room transcript
11e6f38a fix: stop Graph/Topology from hiding nodes despite hop filters
4cdc3587 chore: bump deps
c426f8ff fix: only warn on newer published Ratspeak releases in pnpm run update
cb5299d8 fix: close tcp-write, auto-login, RF-filter, and /who races from review
7b542766 fix: let Graph Max hops work when distant peers are off
d712adc9 fix: stop Graph from treating unknown hops as 1-hop neighbors

75 files, +3378 / −346.

Test plan

Unit coverage landed with the behavior (Graph/Topology hop filters + RF-only + unknown hops, RRC /who session gate + transcript slot + [hub] routing, MeshCore auto-login single-flight + drain breaker, TCP write sentinel, log-service ResizeObserver drop, scripts/update.test.mjs reviewed-ref cases). Suggested manual checks:

  • Graph (MeshCore/Meshtastic): with Show distant off, set Max hops to 2 and 8 — hop-2 peers appear (were a no-op). Unknown-hop nodes stay hidden until Max hops is All and Show distant is on.
  • Graph / Topology: on a large MQTT or RNS map, Show distant on + All hops shows up to 400 nodes; toolbar note and “limit 400” status match. 1-hop neighbors are not dropped behind the old 48 cap.
  • Topology RF only: checking it drops TCP/I2P/Auto hubs; a spoke named like RNode_TCP_* is not kept just because another interface is named RNode. Filter changes do not re-fetch the sidecar.
  • RRC: join a room, leave the RRC tab, come back — chat is not flooded with repeated /who member lists. Nicklist still updates. Unscoped hub notices land in [hub]. Manual /who or Refresh can show one roster line.
  • MeshCore Rooms: connect with several auto-login rooms — only one pathSync/login wave, not one per advert. Disconnect mid-login does not SendLogin on the new connection. After two silent bulk getWaitingMessages timeouts, drain falls back without waiting 45s each time.
  • Meshtastic TCP: yank the TCP radio during traffic — no Electron meshtastic:tcp-write handler [error] spam; connection still detects loss (disconnect event). Frames are not replayed on the next socket.
  • pnpm run update: Ratspeak v1.0.25 and LXMFace at the pinned file SHA print “reviewed; current”, not a warning box.

Summary by CodeRabbit

  • New Features

    • Added RF-only filtering, hop limits, distant-peer visibility, and clear node-limit indicators to topology and peer graphs.
    • Improved Reticulum /who results with hub-wide handling, room-aware rosters, and reduced duplicate transcript messages.
    • Added more reliable MeshCore room auto-login with debouncing and retry coordination.
  • Bug Fixes

    • Improved handling of Meshtastic reconnect races and reduced harmless renderer warning noise.
    • Added fallback behavior after repeated MeshCore waiting-message timeouts.
  • Documentation

    • Documented graph limits, filtering behavior, troubleshooting guidance, and release-review procedures.

…oise

Concurrent MeshCore connect auto-login was racing pathSync on every node-list change; silent bulk getWaitingMessages always waited 45s before fallback. Collapse auto-login to a single flight, skip bulk after consecutive timeouts, and stop logging expected TCP/ResizeObserver races as errors.
Auto /who was gated by a panel ref, so returning to RRC remounted and polled again, dumping member lists into chat. Keep the once-per-join gate on the hub session, show the first roster line only, and route unscoped hub notices to [hub].
Apply the 48-node layout budget only when distant peers are hidden;
use 400 when they are shown. Document the cap, show it on both
panels, and add a Reticulum Topology RF-only filter.
Pin reviewed-ref baselines so already-reviewed tags (Ratspeak v1.0.25) and
repos without GitHub Releases (LXMFace) no longer look like errors. Scan
Four in a Row only when a published release is newer than the pin.
Meshtastic TCP writes now fail in the renderer when the socket is gone so frames are not silently dropped. Room auto-login single-flight survives disconnect, RF-only topology matches configured spokes exactly, and RRC /who no longer skips a full roster after a partial nicklist.
The nearby hop ceiling was ANDed with Max hops, so hop 2/8 was a no-op
on MeshCore/Meshtastic Graph. Apply that ceiling only when Max hops is All.
Unknown hops no longer pass numeric Max hops, and the 48-node cap no
longer hides real 1-hop peers behind Show distant. Layout budget is 400
after hop filters.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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: 27 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: eae02d74-7b09-4e41-a04d-c1e3bb115ee8

📥 Commits

Reviewing files that changed from the base of the PR and between d712adc and 16444c3.

📒 Files selected for processing (31)
  • AGENTS.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • src/main/index.contract.test.ts
  • src/main/index.ts
  • src/main/log-service.test.ts
  • src/main/meshtasticTcpWriteResult.test.ts
  • src/main/meshtasticTcpWriteResult.ts
  • src/renderer/components/ReticulumTopologyPanel.tsx
  • src/renderer/components/RrcPanel.test.tsx
  • src/renderer/components/RrcPanel.tsx
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts
  • src/renderer/lib/reticulum/buildReticulumTopologyLayout.test.ts
  • src/renderer/lib/reticulum/buildReticulumTopologyLayout.ts
  • src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.test.ts
  • src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.ts
  • src/renderer/lib/rrcMessageDisplay.test.ts
  • src/renderer/lib/rrcMessageDisplay.ts
  • src/renderer/lib/rrcRoomName.test.ts
  • src/renderer/lib/rrcRoomName.ts
  • src/renderer/lib/rrcWhoInbound.test.ts
  • src/renderer/lib/rrcWhoInbound.ts
  • src/renderer/lib/topologyGraphLimits.ts
  • src/renderer/lib/transportTcpIpc.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.ts
  • src/renderer/runtime/useReticulumRuntime.rrc.test.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/stores/rrcSessionStore.test.ts
  • src/renderer/stores/rrcSessionStore.ts
📝 Walkthrough

Walkthrough

The PR updates release monitoring, topology limits and filters, RRC /who routing, MeshCore connection workflows, Meshtastic TCP write handling, renderer log filtering, documentation, and package versions.

Changes

Upstream release monitoring

Layer / File(s) Summary
Reviewed release and vendored-file baselines
scripts/update.sh, scripts/update.test.mjs, docs/reticulum-games-parity.md
The update script compares published releases and vendored-file commits with reviewed baselines. Tests cover release, compare, missing-release, and changed-commit cases.

Topology filtering and rendering

Layer / File(s) Summary
Shared topology rules
src/renderer/lib/topologyGraphLimits.ts, src/renderer/lib/reticulum/*, src/renderer/lib/buildMeshPeerTopologyGraph.ts
Shared node caps, hop filtering, RF-only classification, and Reticulum peer selection are added.
Graph construction and panels
src/renderer/lib/buildMeshPeerTopologyGraph.ts, src/renderer/lib/reticulum/buildReticulumTopologyLayout.ts, src/renderer/components/*Topology*, src/renderer/components/PeerGraphPanel.tsx
Graph builders and panels apply shared limits, cache topology data, rebuild on filter changes, expose RF-only controls, and show visible-node limits. Tests cover caps, filters, unknown hops, RF peers, and rendered counts.

RRC /who handling

Layer / File(s) Summary
Room resolution and session state
src/renderer/lib/rrcRoomName.ts, src/renderer/lib/rrcMessageDisplay.ts, src/renderer/stores/rrcSessionStore.ts
RRC room helpers resolve hub messages and joined rooms. Session state tracks automatic requests, transcript slots, forced refreshes, and reset behavior.
Command and runtime flow
src/renderer/components/RrcPanel.tsx, src/renderer/runtime/useReticulumRuntime.ts
/who commands are hub-global. Automatic requests are deduplicated through session state. Inbound rosters update matching joined rooms, and only the permitted transcript snapshot is shown.

MeshCore connection workflows

Layer / File(s) Summary
Room auto-login coordination
src/renderer/lib/meshcoreRoomAutoLoginOnConnect.ts, src/renderer/runtime/useMeshcoreRuntime.ts
Connect-time room auto-login uses readiness debouncing, single-flight execution, generation checks, concurrent eligible logins, retries, and stale-operation cancellation.
Waiting-message drain circuit breaker
src/renderer/lib/meshcoreWaitingMessagesDrain.ts, src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts
Silent bulk retrieval is skipped after two consecutive timeouts. Successful retrieval and reset paths clear the breaker. Tests cover fallback and reconnect behavior.

Transport and logging

Layer / File(s) Summary
Meshtastic TCP write handling
src/main/index.ts, src/preload/index.ts, src/shared/electron-api.types.ts
The main process returns a no-socket sentinel. The preload layer converts it to an error and rejects unexpected results.
Renderer console filtering
src/main/log-service.ts, src/main/log-service.test.ts
Known Chromium ResizeObserver loop warnings are filtered before logging. Unrelated errors remain visible.

Documentation and package updates

Layer / File(s) Summary
Operational documentation
README.md, docs/agents/*, docs/diagnostics.md, docs/reticulum*.md, docs/troubleshooting.md, AGENTS.md, docs/ci-cd.md
Documentation reflects the updated release checks, topology limits, RRC behavior, and MeshCore connection behavior.
Package versions
package.json
@zip.js/zip.js and TypeScript ESLint packages are updated.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fixes for graph hop filters, RRC /who flooding, MeshCore auto-login, and related race conditions.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fixes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Note

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/renderer/runtime/useMeshcoreRuntime.ts (1)

6034-6049: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Invalidate and recheck the auto-login generation at the SendLogin boundary.

A connection-loss or connection-replacement path does not call resetMeshcoreRoomAutoLoginSingleFlight(). Also, a reset can occur after the current stale check and before repeaterRemoteRpcRef.current() executes its callback. The stale pass can then call meshcoreRoomLogin() with connRef.current from a new connection.

  • src/renderer/runtime/useMeshcoreRuntime.ts#L6034-L6049: Recheck abortIfStale inside the serialized callback immediately before meshcoreRoomLogin().
  • src/renderer/runtime/useMeshcoreRuntime.ts#L3274-L3274: Reset the generation from all connection teardown and supersession paths, including connection loss, prepareRfConnect, and unmount cleanup.
🤖 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/runtime/useMeshcoreRuntime.ts` around lines 6034 - 6049, In
src/renderer/runtime/useMeshcoreRuntime.ts lines 6034-6049, recheck
opts?.abortIfStale?.() inside the serialized repeaterRemoteRpcRef.current
callback immediately before meshcoreRoomLogin(), aborting with the existing
stale-login error when invalidated. In
src/renderer/runtime/useMeshcoreRuntime.ts line 3274, update
resetMeshcoreRoomAutoLoginSingleFlight() usage so the generation resets across
every connection teardown or supersession path, including connection loss,
prepareRfConnect, and unmount cleanup.
src/renderer/runtime/useReticulumRuntime.ts (1)

1163-1202: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drop parsed /who notices that do not match a joined room.

When parseRrcWhoNotice() succeeds but rrcWhoNoticeJoinedRoom() returns null, this code continues to addMessage(). A stale response after PART, or a hub notice for an unjoined room, is then persisted and shown in the wire room or [hub].

Return before transcript handling when who exists and whoRoom is null. Keep roster replacement and transcript-slot consumption limited to joined rooms.

🤖 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/runtime/useReticulumRuntime.ts` around lines 1163 - 1202, In the
notice-handling flow around parseRrcWhoNotice and rrcWhoNoticeJoinedRoom, return
before transcript handling when who is present but whoRoom is null. Ensure
unmatched /who notices cannot reach addMessage or wire/[hub] transcript
persistence, while keeping mergeRoomMembers and consumeWhoTranscriptSlot
restricted to joined rooms.
src/renderer/components/RrcPanel.tsx (1)

802-817: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reserve the transcript slot for the requested /who room.

When the active room is general and the user sends /who lobby, this code reserves general. The lobby response can then be suppressed after its first snapshot, while a later general response can appear unexpectedly.

Parse the /who argument with rrcWhoCommandToken(). Reserve force only for the resolved joined room that the command targets.

🤖 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/RrcPanel.tsx` around lines 802 - 817, Update the
`/who` handling in the message-send flow to parse the command argument with
`rrcWhoCommandToken()`, resolve the targeted joined room, and pass that room to
`reserveWhoTranscriptForce` instead of always using `activeRoom`. Only reserve
force when the resolved target is a valid non-DM joined room, while preserving
existing behavior for non-`/who` messages.
🟡 Other comments (4)
AGENTS.md-121-121 (1)

121-121: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe the LXMFace baseline correctly.

Line 121 states that LXMFace uses a published-release watch. scripts/update.sh uses file:js/lxmface.js@<sha> and checks the latest commit that changed that file. State that LXMFace uses a vendored-file commit baseline.

🤖 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 `@AGENTS.md` at line 121, Update the LXMFace description in the “Update script
sync” section to state that its baseline is a vendored-file commit, using the
commit for file:js/lxmface.js and the latest commit that changed that file,
rather than describing it as a published-release watch.
src/renderer/lib/topologyGraphLimits.ts-20-23 (1)

20-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the stale 48-node-cap description.

topologyGraphVisibleNodeCap() always returns the 400-node cap. Hiding distant peers changes hop eligibility. It does not reduce the post-filter layout budget to 48 nodes.

  • src/renderer/lib/topologyGraphLimits.ts#L20-L23: Change the comment to state that the layout budget remains 400 after hop filtering.
  • docs/reticulum.md#L138-L138: Remove the claim that hidden distant peers use a 48-node drawn-graph cap.
  • docs/troubleshooting.md#L1762-L1768: Remove 48 as an active visible-node limit and update the fix guidance.
🤖 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/topologyGraphLimits.ts` around lines 20 - 23, Update the
comment for topologyGraphVisibleNodeCap() to state that the post-hop-filter
layout budget remains 400 nodes; in docs/reticulum.md lines 138-138, remove the
claim that hidden distant peers impose a 48-node drawn-graph cap; in
docs/troubleshooting.md lines 1762-1768, remove 48 as an active visible-node
limit and revise the troubleshooting guidance accordingly.
src/renderer/lib/reticulum/buildReticulumTopologyLayout.ts-270-278 (1)

270-278: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reserve the self-node slot during breadth-first expansion.

Line 255 reserves one cap slot for SELF_ID. Lines 270-278 can still add cap non-self IDs. The caller then renders self plus cap peers, which exceeds the configured visible-node cap by one.

Proposed fix
-    while (queue.length > 0 && visible.size < cap) {
+    const peerBudget = Math.max(0, cap - 1);
+    while (queue.length > 0 && visible.size < peerBudget) {
       const current = queue.shift()!;
       for (const neighbor of adj.get(current) ?? []) {
         if (neighbor === SELF_ID || visited.has(neighbor)) continue;
         if (!filteredIds.includes(neighbor)) continue;
         visited.add(neighbor);
         visible.add(neighbor);
         queue.push(neighbor);
-        if (visible.size >= cap) break;
+        if (visible.size >= peerBudget) break;
       }
     }

Add a layout-overflow test that asserts the final graph, including self, does not exceed the cap.

🤖 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/buildReticulumTopologyLayout.ts` around lines 270
- 278, Update the breadth-first expansion around the queue loop to reserve one
slot for SELF_ID by limiting non-self additions to cap - 1, while preserving
filtering and traversal behavior. Ensure the final rendered graph, including
SELF_ID, never exceeds the configured cap, and add a layout-overflow test
asserting that total graph nodes remain within the cap.
src/main/index.ts-6400-6402 (1)

6400-6402: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle stale socket writes as expected disconnects

A stale socket reference can survive after destroy(). In this state, sock.write() reports ERR_STREAM_DESTROYED or ERR_STREAM_WRITE_AFTER_END, and the handler rejects. Resolve these expected disconnect errors as 'no-socket'. Continue rejecting genuine write failures. Add regression coverage with a destroyed socket after the reference check.

🤖 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/main/index.ts` around lines 6400 - 6402, Update the meshtastic TCP write
handler around the active-socket check to catch write errors indicating a
destroyed or ended socket and resolve them as 'no-socket'. Continue propagating
all other write failures, and add regression coverage using a destroyed socket
after the reference check.
🧹 Nitpick comments (2)
src/renderer/lib/transportTcpIpc.test.ts (1)

80-90: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert that the rejected payload is not replayed.

The test verifies the error, but it does not explicitly assert the no-replay contract. Add a call-count assertion so this failure path cannot issue duplicate writes.

Suggested regression assertion
     await expect(writer.write(new Uint8Array([1, 2, 3]))).rejects.toThrow(
       'meshtastic:tcp-write: no active socket',
     );
+    expect(window.electronAPI.meshtastic.tcp.write).toHaveBeenCalledTimes(1);
     writer.releaseLock();
🤖 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/transportTcpIpc.test.ts` around lines 80 - 90, Add a
call-count assertion to the `toDevice.write` rejection test, verifying
`window.electronAPI.meshtastic.tcp.write` is invoked exactly once after the
rejected `writer.write` call. Keep the existing error assertion and lock cleanup
unchanged.
src/main/log-service.test.ts (1)

417-426: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the optional prefix path.

The tests cover the canonical strings but not the normalization branch in src/main/log-service.ts:412-418. Add an assertion with the optional [Violation] prefix and surrounding whitespace. Otherwise, that behavior can regress while the suite remains green.

Proposed test
     expect(isDroppableRendererConsoleNoise('ResizeObserver loop limit exceeded')).toBe(true);
+    expect(
+      isDroppableRendererConsoleNoise(
+        '  [Violation] ResizeObserver loop completed with undelivered notifications.  ',
+      ),
+    ).toBe(true);
🤖 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/main/log-service.test.ts` around lines 417 - 426, Add a test assertion in
the isDroppableRendererConsoleNoise test for a ResizeObserver warning containing
the optional “[Violation]” prefix and surrounding whitespace, and verify it
returns true. Keep the existing canonical warning assertions unchanged.

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 `@src/renderer/components/RrcPanel.tsx`:
- Around line 276-289: Update requestRoomWho() in
src/renderer/components/RrcPanel.tsx:276-289 to await the send result and
release automatic request state for rejected sends or resolved { ok: false }
results; clear forced transcript state whenever a forced send fails. Update
sendHubCommand() in src/renderer/components/RrcPanel.tsx:241-260 to clear forced
transcript state on unsuccessful completion. Add tests in
src/renderer/components/RrcPanel.test.tsx:651-669 covering resolved { ok: false
} sends and failed forced reservations.

In `@src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts`:
- Around line 312-314: Update drainWaitingMessagesIncremental and the
skipped-drain branch in src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts
(lines 312-314) to propagate successful retrieval and invoke
noteMeshcoreSilentBulkSuccess only after that success, allowing the breaker to
reset. Update src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts (lines
425-452) to open the breaker, complete incremental retrieval successfully, and
verify the next silent drain retries getWaitingMessages(). Update
docs/agents/meshcore-repeaters.md (line 19) only as needed to retain the
reconnect-or-success statement now supported by production behavior.

In `@src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.ts`:
- Around line 21-32: Update selectReticulumTopologyPeersForRender to apply the
active hop and nearby-hop eligibility filters before sorting and enforcing
TOPOLOGY_PEER_RENDER_CAP, reusing the same filtering behavior expected by
buildReticulumMeshTopologyGraph. Preserve rfOnly filtering, then add a
regression test covering more than 800 fresh ineligible peers followed by older
eligible peers and verify the eligible peers are retained.

In `@src/renderer/runtime/useReticulumRuntime.rrc.test.ts`:
- Around line 42-48: Replace the SOURCE string-matching test in the runtime
behavior suite with tests that dispatch rrc.message events through the runtime
or a dedicated testable event-handler helper. Assert joined-room roster
replacement, display of the first /who notice, suppression of subsequent notices
via consumeWhoTranscriptSlot, and rejection of unjoined-room messages, using
observable runtime state or outputs rather than source text.

---

Outside diff comments:
In `@src/renderer/components/RrcPanel.tsx`:
- Around line 802-817: Update the `/who` handling in the message-send flow to
parse the command argument with `rrcWhoCommandToken()`, resolve the targeted
joined room, and pass that room to `reserveWhoTranscriptForce` instead of always
using `activeRoom`. Only reserve force when the resolved target is a valid
non-DM joined room, while preserving existing behavior for non-`/who` messages.

In `@src/renderer/runtime/useMeshcoreRuntime.ts`:
- Around line 6034-6049: In src/renderer/runtime/useMeshcoreRuntime.ts lines
6034-6049, recheck opts?.abortIfStale?.() inside the serialized
repeaterRemoteRpcRef.current callback immediately before meshcoreRoomLogin(),
aborting with the existing stale-login error when invalidated. In
src/renderer/runtime/useMeshcoreRuntime.ts line 3274, update
resetMeshcoreRoomAutoLoginSingleFlight() usage so the generation resets across
every connection teardown or supersession path, including connection loss,
prepareRfConnect, and unmount cleanup.

In `@src/renderer/runtime/useReticulumRuntime.ts`:
- Around line 1163-1202: In the notice-handling flow around parseRrcWhoNotice
and rrcWhoNoticeJoinedRoom, return before transcript handling when who is
present but whoRoom is null. Ensure unmatched /who notices cannot reach
addMessage or wire/[hub] transcript persistence, while keeping mergeRoomMembers
and consumeWhoTranscriptSlot restricted to joined rooms.

---

Other comments:
In `@AGENTS.md`:
- Line 121: Update the LXMFace description in the “Update script sync” section
to state that its baseline is a vendored-file commit, using the commit for
file:js/lxmface.js and the latest commit that changed that file, rather than
describing it as a published-release watch.

In `@src/main/index.ts`:
- Around line 6400-6402: Update the meshtastic TCP write handler around the
active-socket check to catch write errors indicating a destroyed or ended socket
and resolve them as 'no-socket'. Continue propagating all other write failures,
and add regression coverage using a destroyed socket after the reference check.

In `@src/renderer/lib/reticulum/buildReticulumTopologyLayout.ts`:
- Around line 270-278: Update the breadth-first expansion around the queue loop
to reserve one slot for SELF_ID by limiting non-self additions to cap - 1, while
preserving filtering and traversal behavior. Ensure the final rendered graph,
including SELF_ID, never exceeds the configured cap, and add a layout-overflow
test asserting that total graph nodes remain within the cap.

In `@src/renderer/lib/topologyGraphLimits.ts`:
- Around line 20-23: Update the comment for topologyGraphVisibleNodeCap() to
state that the post-hop-filter layout budget remains 400 nodes; in
docs/reticulum.md lines 138-138, remove the claim that hidden distant peers
impose a 48-node drawn-graph cap; in docs/troubleshooting.md lines 1762-1768,
remove 48 as an active visible-node limit and revise the troubleshooting
guidance accordingly.

---

Nitpick comments:
In `@src/main/log-service.test.ts`:
- Around line 417-426: Add a test assertion in the
isDroppableRendererConsoleNoise test for a ResizeObserver warning containing the
optional “[Violation]” prefix and surrounding whitespace, and verify it returns
true. Keep the existing canonical warning assertions unchanged.

In `@src/renderer/lib/transportTcpIpc.test.ts`:
- Around line 80-90: Add a call-count assertion to the `toDevice.write`
rejection test, verifying `window.electronAPI.meshtastic.tcp.write` is invoked
exactly once after the rejected `writer.write` call. Keep the existing error
assertion and lock cleanup unchanged.
🪄 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: ea8c6bc4-891b-4764-ae50-9f047fd31ff5

📥 Commits

Reviewing files that changed from the base of the PR and between 79eedc2 and d712adc.

⛔ Files ignored due to path filters (17)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • 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 (58)
  • AGENTS.md
  • README.md
  • docs/agents/meshcore-repeaters.md
  • docs/agents/meshcore-rooms.md
  • docs/agents/reticulum.md
  • docs/ci-cd.md
  • docs/diagnostics.md
  • docs/reticulum-games-parity.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • package.json
  • scripts/update.sh
  • scripts/update.test.mjs
  • src/main/index.contract.test.ts
  • src/main/index.ts
  • src/main/log-service.test.ts
  • src/main/log-service.ts
  • src/preload/index.ts
  • src/renderer/components/PeerGraphPanel.test.tsx
  • src/renderer/components/PeerGraphPanel.tsx
  • src/renderer/components/ReticulumTopologyPanel.test.tsx
  • src/renderer/components/ReticulumTopologyPanel.tsx
  • src/renderer/components/RrcPanel.test.tsx
  • src/renderer/components/RrcPanel.tsx
  • src/renderer/components/TopologyHopFilterControls.tsx
  • src/renderer/components/TopologyVisibleLimitNote.test.tsx
  • src/renderer/components/TopologyVisibleLimitNote.tsx
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts
  • src/renderer/lib/buildMeshPeerTopologyGraph.test.ts
  • src/renderer/lib/buildMeshPeerTopologyGraph.ts
  • src/renderer/lib/meshcoreRoomAutoLoginOnConnect.test.ts
  • src/renderer/lib/meshcoreRoomAutoLoginOnConnect.ts
  • src/renderer/lib/meshcoreWaitingMessagesDrain.test.ts
  • src/renderer/lib/meshcoreWaitingMessagesDrain.ts
  • src/renderer/lib/meshtastic/meshtasticTransportLossDetection.test.ts
  • src/renderer/lib/meshtastic/meshtasticTransportLossDetection.ts
  • src/renderer/lib/reticulum/buildReticulumTopologyLayout.test.ts
  • src/renderer/lib/reticulum/buildReticulumTopologyLayout.ts
  • src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.test.ts
  • src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.ts
  • src/renderer/lib/reticulum/reticulumTopologyRfFilter.test.ts
  • src/renderer/lib/reticulum/reticulumTopologyRfFilter.ts
  • src/renderer/lib/rrcMessageDisplay.test.ts
  • src/renderer/lib/rrcMessageDisplay.ts
  • src/renderer/lib/rrcNoticeParsers.test.ts
  • src/renderer/lib/rrcRoomName.test.ts
  • src/renderer/lib/rrcRoomName.ts
  • src/renderer/lib/timeConstants.ts
  • src/renderer/lib/topologyGraphLimits.test.ts
  • src/renderer/lib/topologyGraphLimits.ts
  • src/renderer/lib/transportTcpIpc.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.ts
  • src/renderer/runtime/useReticulumRuntime.rrc.test.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/stores/rrcSessionStore.test.ts
  • src/renderer/stores/rrcSessionStore.ts
  • src/shared/electron-api.types.ts

Comment thread src/renderer/components/RrcPanel.tsx Outdated
Comment on lines +312 to +314
if (shouldSkipMeshcoreSilentBulkGetWaitingMessages()) {
await drainWaitingMessagesIncremental(conn, state, deps);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reset the circuit after a successful skipped drain.

After Line 312 returns through drainWaitingMessagesIncremental(), Line 330 cannot call noteMeshcoreSilentBulkSuccess(). The breaker then remains open until a lifecycle reset. This conflicts with the documented “until reconnect/success” behavior and the PR objective to reset after success.

  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts#L312-L314: propagate successful incremental retrieval from drainWaitingMessagesIncremental() and clear the breaker only after that success.
  • docs/agents/meshcore-repeaters.md#L19-L19: retain the reconnect-or-success statement only after the production path supports it.
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts#L425-L452: after opening the breaker, complete an incremental retrieval successfully and verify that the next silent drain retries getWaitingMessages().
📍 Affects 3 files
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts#L312-L314 (this comment)
  • docs/agents/meshcore-repeaters.md#L19-L19
  • src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts#L425-L452
🤖 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/hooks/meshcore/meshcoreConnSideEffects.ts` around lines 312 -
314, Update drainWaitingMessagesIncremental and the skipped-drain branch in
src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts (lines 312-314) to
propagate successful retrieval and invoke noteMeshcoreSilentBulkSuccess only
after that success, allowing the breaker to reset. Update
src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts (lines 425-452) to
open the breaker, complete incremental retrieval successfully, and verify the
next silent drain retries getWaitingMessages(). Update
docs/agents/meshcore-repeaters.md (line 19) only as needed to retain the
reconnect-or-success statement now supported by production behavior.

Comment thread src/renderer/lib/reticulum/reticulumTopologyPeerRenderSelect.ts
Comment on lines +42 to +48
it('suppresses later /who notices after mergeRoomMembers and consumeWhoTranscriptSlot', () => {
expect(SOURCE).toContain('resolveRrcInboundChatRoom');
expect(SOURCE).toMatch(/room = resolveRrcInboundChatRoom\(/);
expect(SOURCE).toMatch(/mergeRoomMembers\(whoRoom, who\.members, 'replace', hubDestHash\)/);
expect(SOURCE).toMatch(/consumeWhoTranscriptSlot\(whoRoom, hubDestHash\)[\s\S]*?return;/);
expect(SOURCE).toMatch(/if \(who && whoRoom\) \{[\s\S]*?room = whoRoom/);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Replace source-text assertions with runtime behavior assertions.

This test passes when the expected strings remain in the source, even if event control flow no longer updates the roster or suppresses the transcript correctly.

Dispatch rrc.message events through the runtime, or extract the event handling into a testable helper. Assert joined-room replacement, first-notice display, later-notice suppression, and unjoined-room rejection.

As per coding guidelines, “Ship a passing test for behavioral changes.” As per path instructions, “Prefer behavioral assertions; skip style-only test nits.”

🤖 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/runtime/useReticulumRuntime.rrc.test.ts` around lines 42 - 48,
Replace the SOURCE string-matching test in the runtime behavior suite with tests
that dispatch rrc.message events through the runtime or a dedicated testable
event-handler helper. Assert joined-room roster replacement, display of the
first /who notice, suppression of subsequent notices via
consumeWhoTranscriptSlot, and rejection of unjoined-room messages, using
observable runtime state or outputs rather than source text.

Sources: Coding guidelines, Path instructions

Await RRC /who send results, hop-filter Topology ingest before the 800 cap,
reset the MeshCore silent-bulk breaker after incremental success, and treat
destroyed Meshtastic TCP sockets as no-socket.
@rinchen
rinchen merged commit e5e0dd1 into main Aug 11, 2026
12 checks passed
@rinchen
rinchen deleted the fixes branch August 11, 2026 15:32
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