diff --git a/.agents/skills/reticulum-design-gates/SKILL.md b/.agents/skills/reticulum-design-gates/SKILL.md index d3538e0d..3d64bef2 100644 --- a/.agents/skills/reticulum-design-gates/SKILL.md +++ b/.agents/skills/reticulum-design-gates/SKILL.md @@ -55,6 +55,7 @@ Allowed: - Prefer event/handler and store-and-forward over blocking request/response UIs. - Missing path: request path, allow propagate, surface recoverable error. Do not spin forever. +- Path and first-hop link waits use `path_utils.path_response_window` and `link.establishment_timeout`. Do not pin 15s (or any flat timer) for Nomad pages, RNCP, FileSync, LXST, or map fetches. - Keep list APIs and announces slim. Do not ship multi-MB blobs in conversation lists. - Large files use RNCP / attachments / explicit transfer tools, not chat text fields. diff --git a/.agents/skills/reticulum-stack/SKILL.md b/.agents/skills/reticulum-stack/SKILL.md index 05f2c7ed..12969882 100644 --- a/.agents/skills/reticulum-stack/SKILL.md +++ b/.agents/skills/reticulum-stack/SKILL.md @@ -59,6 +59,11 @@ Announce ingest caps and store toggles live in `announce_manager.py` (`announce_ Missing path: request a path, allow LXMF propagate where that is the protocol, show a recoverable outbound state. Do not spin the UI until an ACK arrives on a LoRa-class link. +Do not pin a 15 second (or any fixed) timer for cold path requests or first-hop link setup. Reticulum already knows the interface bitrate. + +- Path wait: `meshchatx/src/backend/path_utils.py` `path_response_window`. Uses `RNS.Reticulum.get_instance().get_first_hop_timeout()` (not `RNS.Transport.first_hop_timeout()`, which is wrong on a shared rnsd client) plus an airtime floor from the slowest online interface bitrate, clamped to `RNS.Reticulum.MINIMUM_BITRATE` (5 bps). +- Link wait: `link.establishment_timeout` plus `LINK_ESTABLISHMENT_MARGIN_S` via `link_establishment_window`. Pass `None` so callers do not override RNS. + Links are live sessions on top of paths. LXST calls and RRC hubs use links. LXMF mail is store-and-forward and must survive a missing path. ## Key files diff --git a/meshchatx.rsm b/meshchatx.rsm index a5657a24..decee439 100644 Binary files a/meshchatx.rsm and b/meshchatx.rsm differ diff --git a/meshchatx/src/backend/bug_report_manager.py b/meshchatx/src/backend/bug_report_manager.py index 746d13eb..6ad04ace 100644 --- a/meshchatx/src/backend/bug_report_manager.py +++ b/meshchatx/src/backend/bug_report_manager.py @@ -14,6 +14,10 @@ import RNS from meshchatx.src.backend.announce_handler import AnnounceHandler from meshchatx.src.backend.log_redaction import redact_diagnostic_text +from meshchatx.src.backend.path_utils import ( + link_establishment_window, + path_response_window, +) BUG_ASPECT = "mcx-bugs-v1" REPORT_PATH = "/report" @@ -440,7 +444,10 @@ class BugReportManager: ) if not RNS.Transport.has_path(dest_hash): RNS.Transport.request_path(dest_hash) - deadline = time.time() + float(args.get("path_timeout") or 15) + path_wait = float(args.get("path_timeout") or 0) or path_response_window( + dest_hash, + ) + deadline = time.time() + path_wait while time.time() < deadline: if RNS.Transport.has_path(dest_hash): break @@ -468,7 +475,11 @@ class BugReportManager: response_event.set() link.set_link_established_callback(on_established) - if not established.wait(timeout=float(args.get("link_timeout") or 20)): + link_wait = float(args.get("link_timeout") or 0) or link_establishment_window( + link, + dest_hash, + ) + if not established.wait(timeout=link_wait): try: link.teardown() except Exception: diff --git a/meshchatx/src/backend/http/routes/path_probe.py b/meshchatx/src/backend/http/routes/path_probe.py index eed4bbc6..181257e0 100644 --- a/meshchatx/src/backend/http/routes/path_probe.py +++ b/meshchatx/src/backend/http/routes/path_probe.py @@ -3,7 +3,6 @@ from __future__ import annotations - from meshchatx.src.backend.http.meshchat_names import ( # noqa: F401 GeoValidationError, OutboundHttpBlockedError, @@ -131,6 +130,8 @@ from meshchatx.src.backend.http.meshchat_names import ( # noqa: F401 zipfile, ) +from meshchatx.src.backend.path_utils import path_response_window + # Same ceiling as RNProbeHandler.MAX_TIMEOUT_S PATH_PROBE_MIN_TIMEOUT_S = 1 PATH_PROBE_MAX_TIMEOUT_S = 600 @@ -139,9 +140,11 @@ PATH_WAIT_REQUIRES_POST_MESSAGE = ( ) -def parse_path_probe_timeout(raw, *, default=15): +def parse_path_probe_timeout(raw, *, default=None): if raw is None or raw == "": raw = default + if raw is None or raw == "": + return None, None try: timeout_seconds = int(raw) except (TypeError, ValueError): @@ -157,7 +160,7 @@ def parse_path_probe_timeout(raw, *, default=15): return timeout_seconds, None -async def read_path_probe_timeout_raw(request, default=15): +async def read_path_probe_timeout_raw(request, default=None): query = getattr(request, "query", None) or {} if "timeout" in query: return query.get("timeout") @@ -303,10 +306,16 @@ def register_path_probe_routes(routes, app): ) destination_hash_hex = destination_hash_bytes.hex() - timeout_raw = await read_path_probe_timeout_raw(request, default=15) + timeout_raw = await read_path_probe_timeout_raw(request) timeout_seconds, timeout_error = parse_path_probe_timeout(timeout_raw) if timeout_error: return web.json_response({"message": timeout_error}, status=400) + if timeout_seconds is None: + reticulum = app.reticulum if hasattr(app, "reticulum") else None + timeout_seconds = path_response_window( + destination_hash_bytes, + reticulum, + ) if destination_hash_hex in local_destination_hashes(app): return local_path_response(destination_hash_hex) @@ -483,13 +492,18 @@ def register_path_probe_routes(routes, app): status=400, ) - timeout_raw = await read_path_probe_timeout_raw(request, default=15) + timeout_raw = await read_path_probe_timeout_raw(request) timeout_seconds, timeout_error = parse_path_probe_timeout(timeout_raw) if timeout_error: return web.json_response( {"message": f"Ping failed. {timeout_error}"}, status=400, ) + if timeout_seconds is None: + reticulum = app.reticulum if hasattr(app, "reticulum") else None + timeout_seconds = int( + round(path_response_window(destination_hash, reticulum)), + ) # Split the budget so path discovery cannot consume the whole timeout. path_budget_seconds = max(1, timeout_seconds // 2) diff --git a/meshchatx/src/backend/http/routes/rn_tools.py b/meshchatx/src/backend/http/routes/rn_tools.py index d1cca213..5a988f4c 100644 --- a/meshchatx/src/backend/http/routes/rn_tools.py +++ b/meshchatx/src/backend/http/routes/rn_tools.py @@ -436,7 +436,11 @@ def register_rn_tools_routes(routes, app): data = await request.json() destination_hash_str = data.get("destination_hash", "") file_path = data.get("file_path", "") - timeout = float(data.get("timeout", RNS.Transport.PATH_REQUEST_TIMEOUT)) + timeout_raw = data.get("timeout") + try: + timeout = float(timeout_raw) if timeout_raw not in (None, "") else None + except (TypeError, ValueError): + timeout = None no_compress = bool(data.get("no_compress", False)) try: @@ -496,7 +500,11 @@ def register_rn_tools_routes(routes, app): data = await request.json() destination_hash_str = data.get("destination_hash", "") file_path = data.get("file_path", "") - timeout = float(data.get("timeout", RNS.Transport.PATH_REQUEST_TIMEOUT)) + timeout_raw = data.get("timeout") + try: + timeout = float(timeout_raw) if timeout_raw not in (None, "") else None + except (TypeError, ValueError): + timeout = None save_path = data.get("save_path") allow_overwrite = bool(data.get("allow_overwrite", False)) diff --git a/meshchatx/src/backend/map_data_manager.py b/meshchatx/src/backend/map_data_manager.py index c9e61544..eef6cc58 100644 --- a/meshchatx/src/backend/map_data_manager.py +++ b/meshchatx/src/backend/map_data_manager.py @@ -22,6 +22,7 @@ from meshchatx.src.backend.map_geo_validator import ( validate_geo_bytes, ) from meshchatx.src.backend.map_overlay_manager import atomic_write_bytes +from meshchatx.src.backend.path_utils import path_response_window from meshchatx.src.path_utils import is_path_within_dir _log = logging.getLogger("meshchatx.map_data") @@ -545,6 +546,13 @@ class MapDataManager: if link_manager is None: raise MapDataError("link_unavailable") path_timeout = int(self.config.map_overlay_path_timeout_seconds.get() or 30) + try: + path_timeout = max( + float(path_timeout), + path_response_window(destination_hash, self.reticulum), + ) + except Exception: + path_timeout = float(path_timeout) transfer_timeout = int( self.config.map_overlay_transfer_timeout_seconds.get() or 120 ) @@ -553,7 +561,6 @@ class MapDataManager: destination_hash, MAP_ASPECT, path_lookup_timeout=float(path_timeout), - link_establishment_timeout=float(path_timeout), ) if link is None: code = ( diff --git a/meshchatx/src/backend/map_geo_sanitizer.py b/meshchatx/src/backend/map_geo_sanitizer.py index 3f3c26fe..4630d996 100644 --- a/meshchatx/src/backend/map_geo_sanitizer.py +++ b/meshchatx/src/backend/map_geo_sanitizer.py @@ -5,6 +5,7 @@ from __future__ import annotations import json +import re import zipfile from dataclasses import dataclass, field from io import BytesIO @@ -114,6 +115,14 @@ def _set_href_on_element(el: ET.Element, value: str | None) -> None: el.attrib[key] = value +_HTML_TAG_RE = re.compile(r"<[^>]+>") +_WS_RE = re.compile(r"\s+") + + +def _flatten_html_text(text: str) -> str: + return _WS_RE.sub(" ", _HTML_TAG_RE.sub(" ", text)).strip() + + def _strip_description_html(el: ET.Element) -> bool: tag = _strip_ns(el.tag).lower() if tag != "description": @@ -129,8 +138,13 @@ def _strip_description_html(el: ET.Element) -> bool: if child.tail and child.tail.strip(): texts.append(child.tail.strip()) el.remove(child) - el.text = " ".join(texts) if texts else None - return had_children + combined = " ".join(texts) + changed = had_children + if "<" in combined and ">" in combined: + combined = _flatten_html_text(combined) + changed = True + el.text = combined or None + return changed def _walk_strip_kml( @@ -277,7 +291,10 @@ def sanitize_kmz_bytes(data: bytes) -> SanitizeResult: raise GeoValidationError("path_traversal") ext = _zip_entry_ext(name) if ext not in ALLOWED_KMZ_EXTS: - raise GeoValidationError("unsafe_kmz_entry") + # ArcGIS KMZ exports include unused .xsl balloon stylesheets. + # Drop sidecars instead of rejecting the archive. + stripped.append("skipped_kmz_entry") + continue payload = zf.read(info.filename) kept[name] = payload lower = name.lower() diff --git a/meshchatx/src/backend/map_overlay_manager.py b/meshchatx/src/backend/map_overlay_manager.py index d0933c49..840d358d 100644 --- a/meshchatx/src/backend/map_overlay_manager.py +++ b/meshchatx/src/backend/map_overlay_manager.py @@ -38,6 +38,7 @@ from meshchatx.src.backend.map_overlay_sources import ( parse_create_payload, ) from meshchatx.src.backend.nomadnet_downloader import NomadnetFileDownloader +from meshchatx.src.backend.path_utils import path_response_window from meshchatx.src.backend.rngit_sparse_fetcher import ( RngitFetchError, RngitSparseFetcher, @@ -623,6 +624,14 @@ class MapOverlayManager: generation: int, ) -> None: path_timeout = self._cfg_int("map_overlay_path_timeout_seconds") + try: + dest = bytes.fromhex(spec.destination_hash) + path_timeout = max( + float(path_timeout), + path_response_window(dest, self.reticulum), + ) + except Exception: + path_timeout = float(path_timeout) transfer_timeout = self._cfg_int("map_overlay_transfer_timeout_seconds") job_timeout = self._cfg_int("map_overlay_job_timeout_seconds") @@ -662,7 +671,6 @@ class MapOverlayManager: await asyncio.wait_for( downloader.download( path_lookup_timeout=path_timeout, - link_establishment_timeout=path_timeout, ), timeout=job_timeout, ) diff --git a/meshchatx/src/backend/nomadnet_downloader.py b/meshchatx/src/backend/nomadnet_downloader.py index 5ded9a2e..2c7e076b 100644 --- a/meshchatx/src/backend/nomadnet_downloader.py +++ b/meshchatx/src/backend/nomadnet_downloader.py @@ -10,7 +10,10 @@ from collections.abc import Callable import RNS from meshchatx.src.backend import reticulum_pathfinding -from meshchatx.src.backend.path_utils import path_response_window +from meshchatx.src.backend.path_utils import ( + link_establishment_window, + path_response_window, +) from meshchatx.src.backend.reticulum_pathfinding import ReticulumLike # Global cache for Nomad Network links (reuse instead of reconnecting per request). @@ -293,14 +296,11 @@ class NomadnetDownloader: self.link = link if link_establishment_timeout is None: - rns_timeout = getattr(link, "establishment_timeout", None) - if isinstance(rns_timeout, (int, float)) and rns_timeout > 0: - link_establishment_timeout = rns_timeout + 5 - else: - link_establishment_timeout = path_response_window( - self.destination_hash, - self._reticulum, - ) + link_establishment_timeout = link_establishment_window( + link, + self.destination_hash, + self._reticulum, + ) timeout_after_seconds = time.time() + link_establishment_timeout while ( diff --git a/meshchatx/src/backend/path_utils.py b/meshchatx/src/backend/path_utils.py index 449c2714..55dcd962 100644 --- a/meshchatx/src/backend/path_utils.py +++ b/meshchatx/src/backend/path_utils.py @@ -2,8 +2,24 @@ import RNS -MIN_WINDOW_BITRATE = 50 +# One path-request packet is about 234 bytes plus IFAC. 240 covers that. PATH_EXCHANGE_BYTES = 240 +LINK_ESTABLISHMENT_MARGIN_S = 5.0 +_FALLBACK_PATH_TIMEOUT_S = 15.0 + + +def min_window_bitrate() -> float: + try: + value = float(RNS.Reticulum.MINIMUM_BITRATE) + if value > 0: + return value + except Exception: + pass + return 5.0 + + +# Kept as a name for tests and callers. Equals RNS.Reticulum.MINIMUM_BITRATE (5). +MIN_WINDOW_BITRATE = min_window_bitrate() def slowest_online_bitrate(reticulum=None): @@ -23,14 +39,49 @@ def slowest_online_bitrate(reticulum=None): return None +def _rns_path_request_timeout() -> float: + try: + return float(RNS.Transport.PATH_REQUEST_TIMEOUT) + except Exception: + return _FALLBACK_PATH_TIMEOUT_S + + def path_response_window(destination_hash, reticulum=None) -> float: - if reticulum is None: - reticulum = RNS.Reticulum.get_instance() - window = float(reticulum.get_first_hop_timeout(destination_hash)) + """Seconds to wait for a cold path response on the slowest online interface. + + Uses Reticulum.get_first_hop_timeout so a shared rnsd client sees the + instance interface timeouts, not the local socket timeout. + """ + window = 0.0 + try: + if reticulum is None: + reticulum = RNS.Reticulum.get_instance() + window = float(reticulum.get_first_hop_timeout(destination_hash)) + except Exception: + window = 0.0 bitrate = slowest_online_bitrate(reticulum) if bitrate: + floor_bps = min_window_bitrate() window = max( window, - 2 * (PATH_EXCHANGE_BYTES * 8 / max(bitrate, MIN_WINDOW_BITRATE)) + 10, + 2 * (PATH_EXCHANGE_BYTES * 8 / max(float(bitrate), floor_bps)) + 10, ) - return max(window, float(RNS.Transport.PATH_REQUEST_TIMEOUT)) + return max(window, _rns_path_request_timeout()) + + +def link_establishment_window( + link, + destination_hash=None, + reticulum=None, +) -> float: + """Seconds to wait for a new RNS Link, from link.establishment_timeout. + + Adds LINK_ESTABLISHMENT_MARGIN_S. Falls back to path_response_window + when RNS did not report an establishment timeout. + """ + rns_timeout = getattr(link, "establishment_timeout", None) + if isinstance(rns_timeout, (int, float)) and rns_timeout > 0: + return float(rns_timeout) + LINK_ESTABLISHMENT_MARGIN_S + if destination_hash is not None: + return path_response_window(destination_hash, reticulum) + return _rns_path_request_timeout() diff --git a/meshchatx/src/backend/remote_management_client.py b/meshchatx/src/backend/remote_management_client.py index 7e6aebe3..c52cfa17 100644 --- a/meshchatx/src/backend/remote_management_client.py +++ b/meshchatx/src/backend/remote_management_client.py @@ -16,6 +16,10 @@ from typing import Any import RNS from meshchatx.src.backend.management_identities import resolve_identity_path +from meshchatx.src.backend.path_utils import ( + link_establishment_window, + path_response_window, +) def _truncated_hash_len() -> int: @@ -140,9 +144,16 @@ class _RemoteRequest: self._link = RNS.Link(destination) self._link.set_link_established_callback(on_established) self._link.set_link_closed_callback(on_closed) + request_wait = max( + float(self.timeout), + link_establishment_window( + self._link, + self.destination_hash, + ), + ) try: - if not self._event.wait(timeout=max(1.0, float(self.timeout))): + if not self._event.wait(timeout=max(1.0, request_wait)): raise TimeoutError("Remote management request timed out") if self._error is not None: raise self._error @@ -173,9 +184,7 @@ def remote_request( if identity is None: raise ValueError(f"Could not load management identity from {resolved}") wait = ( - float(timeout) - if timeout not in (None, "") - else float(RNS.Transport.PATH_REQUEST_TIMEOUT) + float(timeout) if timeout not in (None, "") else path_response_window(dest_hash) ) return _RemoteRequest(dest_hash, identity, path, data, wait).run() diff --git a/meshchatx/src/backend/reticulum_pathfinding.py b/meshchatx/src/backend/reticulum_pathfinding.py index c033f4a6..96faad86 100644 --- a/meshchatx/src/backend/reticulum_pathfinding.py +++ b/meshchatx/src/backend/reticulum_pathfinding.py @@ -8,6 +8,8 @@ from typing import Any, Optional, Protocol import RNS +from meshchatx.src.backend.path_utils import path_response_window + @dataclass(frozen=True) class OutboundPathOutcome: @@ -233,7 +235,12 @@ def nudge_path_request(destination_hash: bytes) -> None: RNS.Transport.request_path(destination_hash) -def lxmf_path_wait_cap_seconds() -> float: +def lxmf_path_wait_cap_seconds( + destination_hash: bytes | None = None, + reticulum: Optional["ReticulumLike"] = None, +) -> float: + if destination_hash is not None: + return path_response_window(destination_hash, reticulum) try: base = float(RNS.Transport.PATH_REQUEST_TIMEOUT) except Exception: @@ -245,8 +252,12 @@ async def await_transport_path_for_outbound_lxmf( reticulum: Optional["ReticulumLike"], destination_hash_bytes: bytes, ) -> OutboundPathOutcome: - long_w = lxmf_path_wait_cap_seconds() - short_w = max(15.0, long_w * 0.5) + long_w = lxmf_path_wait_cap_seconds(destination_hash_bytes, reticulum) + try: + short_floor = float(RNS.Transport.PATH_REQUEST_TIMEOUT) + except Exception: + short_floor = 15.0 + short_w = max(long_w * 0.5, short_floor) measure = prepare_fresh_path_request(reticulum, destination_hash_bytes) deadline = time.time() + long_w diff --git a/meshchatx/src/backend/rncp_handler.py b/meshchatx/src/backend/rncp_handler.py index b4ec7d23..68fd5523 100644 --- a/meshchatx/src/backend/rncp_handler.py +++ b/meshchatx/src/backend/rncp_handler.py @@ -9,7 +9,7 @@ from collections.abc import Callable import RNS -from .path_utils import path_response_window +from .path_utils import link_establishment_window, path_response_window class RNCPHandler: @@ -38,6 +38,16 @@ class RNCPHandler: except Exception: pass + def _path_wait_seconds( + self, + destination_hash: bytes, + timeout: float | None, + ) -> float: + window = path_response_window(destination_hash, self.reticulum) + if timeout is None: + return window + return max(float(timeout), window) + def _default_fetch_save_dir(self) -> str: path = os.path.join(self.storage_dir, "rncp", "downloads") os.makedirs(path, exist_ok=True) @@ -398,9 +408,8 @@ class RNCPHandler: if not RNS.Transport.has_path(destination_hash): RNS.Transport.request_path(destination_hash) - if timeout is None: - timeout = path_response_window(destination_hash, self.reticulum) - timeout_after = time.time() + timeout + path_wait = self._path_wait_seconds(destination_hash, timeout) + timeout_after = time.time() + path_wait while ( not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after ): @@ -420,7 +429,11 @@ class RNCPHandler: ) link = RNS.Link(receiver_destination) - timeout_after = time.time() + timeout + timeout_after = time.time() + link_establishment_window( + link, + destination_hash, + self.reticulum, + ) while link.status != RNS.Link.ACTIVE and time.time() < timeout_after: await asyncio.sleep(0.1) @@ -501,9 +514,8 @@ class RNCPHandler: if not RNS.Transport.has_path(destination_hash): RNS.Transport.request_path(destination_hash) - if timeout is None: - timeout = path_response_window(destination_hash, self.reticulum) - timeout_after = time.time() + timeout + path_wait = self._path_wait_seconds(destination_hash, timeout) + timeout_after = time.time() + path_wait while ( not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after ): @@ -523,7 +535,11 @@ class RNCPHandler: ) link = RNS.Link(listener_destination) - timeout_after = time.time() + timeout + timeout_after = time.time() + link_establishment_window( + link, + destination_hash, + self.reticulum, + ) while link.status != RNS.Link.ACTIVE and time.time() < timeout_after: await asyncio.sleep(0.1) diff --git a/meshchatx/src/backend/rnprobe_handler.py b/meshchatx/src/backend/rnprobe_handler.py index 0b219b02..de324e8b 100644 --- a/meshchatx/src/backend/rnprobe_handler.py +++ b/meshchatx/src/backend/rnprobe_handler.py @@ -74,8 +74,8 @@ class RNProbeHandler: timeout_after = time.time() + ( timeout - or self.DEFAULT_TIMEOUT - + self.reticulum.get_first_hop_timeout(destination_hash) + if timeout is not None + else path_response_window(destination_hash, self.reticulum) ) while ( not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after diff --git a/meshchatx/src/backend/rns_link_manager.py b/meshchatx/src/backend/rns_link_manager.py index 528d4036..c8f80754 100644 --- a/meshchatx/src/backend/rns_link_manager.py +++ b/meshchatx/src/backend/rns_link_manager.py @@ -10,7 +10,10 @@ from typing import Optional import RNS from meshchatx.src.backend import reticulum_pathfinding -from meshchatx.src.backend.path_utils import path_response_window +from meshchatx.src.backend.path_utils import ( + link_establishment_window, + path_response_window, +) from meshchatx.src.backend.reticulum_pathfinding import ReticulumLike # Cache of established RNS Links keyed by (aspect_str, destination_hash_bytes). @@ -38,10 +41,8 @@ LINK_IDLE_TTL_S = 30 * 60 # Wait granularity while polling for path / link (seconds). _POLL_INTERVAL_S = 0.02 -# Slow path and link margins +# Slow path UI phase margin before finding_path_slow PATH_MARGIN_S = 5.0 -LINK_MARGIN_S = 5.0 -_FALLBACK_LINK_TIMEOUT_S = 15.0 def cached_link_count() -> int: @@ -368,13 +369,14 @@ class RnsLinkManager: ) # Get the link establishment_timeout from RNS if not explicitly pinned - rns_timeout = getattr(link, "establishment_timeout", None) if link_establishment_timeout is not None: deadline = time.time() + link_establishment_timeout - elif isinstance(rns_timeout, (int, float)) and rns_timeout > 0: - deadline = time.time() + rns_timeout + LINK_MARGIN_S else: - deadline = time.time() + _FALLBACK_LINK_TIMEOUT_S + deadline = time.time() + link_establishment_window( + link, + destination_hash, + self._get_reticulum(), + ) try: while ( link.status not in (RNS.Link.ACTIVE, RNS.Link.CLOSED) diff --git a/meshchatx/src/backend/rrc/manager.py b/meshchatx/src/backend/rrc/manager.py index bcbb7627..f8dd8a91 100644 --- a/meshchatx/src/backend/rrc/manager.py +++ b/meshchatx/src/backend/rrc/manager.py @@ -315,6 +315,7 @@ class RRCHub: if gate is not None: gate.acquire() try: + path_wait = timeout_s if not RNS.Transport.has_path(self.hub_hash): RNS.Transport.request_path(self.hub_hash) try: @@ -328,7 +329,7 @@ class RRCHub: time.sleep(0.1) hub_identity = None - deadline = time.monotonic() + timeout_s + deadline = time.monotonic() + path_wait while time.monotonic() < deadline: hub_identity = RNS.Identity.recall(self.hub_hash) if hub_identity is not None: diff --git a/meshchatx/src/backend/telephone_manager.py b/meshchatx/src/backend/telephone_manager.py index 3277fe6d..9e7a81d3 100644 --- a/meshchatx/src/backend/telephone_manager.py +++ b/meshchatx/src/backend/telephone_manager.py @@ -15,6 +15,7 @@ from meshchatx.src.backend.meshchat_utils import ( hex_identifier_to_bytes, normalize_hex_identifier, ) +from meshchatx.src.backend.path_utils import path_response_window class Tee: @@ -638,9 +639,17 @@ class TelephoneManager: if not RNS.Transport.has_path(call_destination_hash): self._update_initiation_status("Requesting path...") + path_wait = float(timeout_seconds) + try: + path_wait = max( + path_wait, + path_response_window(call_destination_hash), + ) + except Exception: + pass has_path = await self._await_path( call_destination_hash, - timeout_seconds=min(timeout_seconds, 10), + timeout_seconds=path_wait, ) if self._is_initiation_cancelled(): return None diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index 4d4a282e..740a2d48 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -226,8 +226,11 @@ >