From 2cba0cf68339c74741391c3459d0daf1f828cdf4 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sat, 15 Aug 2026 17:36:31 -0500 Subject: [PATCH] fix: keep local Relay Chat commands above hub notices --- meshchatx.rsm | Bin 196008 -> 196008 bytes meshchatx/src/backend/rrc/manager.py | 50 +++++++++++++++++---------- tests/backend/test_rrc_server.py | 26 ++++++++++++++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/meshchatx.rsm b/meshchatx.rsm index 7fe8e3478c7cf8d5a7bef0c838185e2d03d43b49..7a958cdeb316feff1cd0dc3e9b3c803f8f5837d3 100644 GIT binary patch delta 268 zcmV+n0rUQ-`wOW13qUDXfH+uGDr)lc9gU*$2(y&E_6${8mQO*p7-fy>1q=n1u5S_F zLEHbdN$AkN?AG8w+~oDuZVFHaaHE^F6b+F;7$D(iC?l`w;*3I_$#&Id1G_>PHEhHC zcKX1OT*BD}K`OC$@xMPbH8?OiVP;`rGdE>9Heq3AIASw6GB`3YIW#gfVK`$lGd5#6 zW;8Z6G&N;qHaKBqHZ?LbG&nOjV>DzrFfo(SzZ`?az_-M}0l1GpFk~??VPa)CVmUT2 zH8x>5I5}oHHaTH1V=*%_Wi~lAF*h^t0@JL-GiT8N delta 268 zcmV+n0rUQ-`wOW13qW-R97#DgfB2(qcXqH!G--9dW9ki?E3GeAr7E=ducp@VO)wKE z+Z#I1Eu_hYY5%GOsGlwD(0UjnGMjrpCJ2#07$EqSeQa=vBk@Yf>BOrGy=81@xMPaVr4L7I5jabG&nLaF*Rg4IWuH4WimK0W?^PHGi5PjI5{vf zGGR7kI59P0Vm2~jG-76DVq!QpVKiklI5?BhzZ`?az_-M}0l1GpGcqw`IAt?6W->W7 zI51*lGcq^t0@JKJL1-`l diff --git a/meshchatx/src/backend/rrc/manager.py b/meshchatx/src/backend/rrc/manager.py index 63207d3f..6a28895f 100644 --- a/meshchatx/src/backend/rrc/manager.py +++ b/meshchatx/src/backend/rrc/manager.py @@ -606,6 +606,25 @@ class RRCHub: raise RuntimeError(msg) RNS.Packet(link, payload).send() + def _send_env_then_maybe_record(self, env, local_msg): + """Send on the wire, then record local history if send succeeded. + + Mesh send is async, so the local echo still lands before a remote + reply. Loopback delivery is synchronous, so the echo is recorded + first or the hub notice would appear above the typed command. + """ + with self._lock: + link = self.link + loopback = isinstance(link, _LoopbackEndpoint) + if link is None or link.status != RNS.Link.ACTIVE: + msg = "not connected" + raise RuntimeError(msg) + if local_msg is not None and loopback: + self._record_message(local_msg, local=True) + self._send_env(env) + if local_msg is not None and not loopback: + self._record_message(local_msg, local=True) + def join_room(self, room, key=None, silent=False): r = proto.normalize_room(room) body = None @@ -654,20 +673,17 @@ class RRCHub: ) if nick: env[proto.K_NICK] = nick - self._send_env(env) + local_msg = None if record_local: - history_text = self._redact_command_for_history(text) - self._record_message( - proto.RRCMessage( - "msg", - r, - self.manager.identity.hash, - nick, - history_text, - proto.now_ms(), - ), - local=True, + local_msg = proto.RRCMessage( + "msg", + r, + self.manager.identity.hash, + nick, + self._redact_command_for_history(text), + proto.now_ms(), ) + self._send_env_then_maybe_record(env, local_msg) @staticmethod def _redact_command_for_history(text): @@ -735,8 +751,8 @@ class RRCHub: mid = env[proto.K_ID] if isinstance(mid, (bytes, bytearray)): self._sent_ids.append(bytes(mid)) - self._send_env(env) - self._record_message( + self._send_env_then_maybe_record( + env, proto.RRCMessage( "msg", r, @@ -745,7 +761,6 @@ class RRCHub: text, proto.now_ms(), ), - local=True, ) return mid @@ -769,8 +784,8 @@ class RRCHub: mid = env[proto.K_ID] if isinstance(mid, (bytes, bytearray)): self._sent_ids.append(bytes(mid)) - self._send_env(env) - self._record_message( + self._send_env_then_maybe_record( + env, proto.RRCMessage( "action", r, @@ -779,7 +794,6 @@ class RRCHub: text, proto.now_ms(), ), - local=True, ) return mid diff --git a/tests/backend/test_rrc_server.py b/tests/backend/test_rrc_server.py index 2eb2d30d..0f05d1b2 100644 --- a/tests/backend/test_rrc_server.py +++ b/tests/backend/test_rrc_server.py @@ -400,6 +400,32 @@ def test_loopback_nick_command_updates_override(tmp_path): assert msgs[-1].text == "nickname set to bob" +def test_mesh_command_not_recorded_when_send_fails(tmp_path): + _, hub = _loopback_client_hub(tmp_path) + hub.messages["general"] = [] + status = hub.link.status + + class MeshLink: + pass + + mesh = MeshLink() + mesh.status = status + hub.link = mesh + + def boom(_env): + raise RuntimeError("link down") + + hub._send_env = boom + raised = None + try: + hub.send_command("/help", room="general") + except RuntimeError as exc: + raised = exc + assert raised is not None + assert str(raised) == "link down" + assert hub.messages["general"] == [] + + def test_server_members_dict_all_and_per_room(tmp_path): server = make_running_server() server.register_room("general")