From 4bb570d3c4118bde3d774ca130688f9bb3de1eb7 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Fri, 13 Mar 2026 18:06:50 -0400 Subject: [PATCH 1/6] fix: filter partial form fields and fix centered text in scroll mode PartialManager now only forwards form fields declared by each partial's field list, matching NomadNet TUI's __get_partial_request_data behavior. "*" forwards all fields; an empty list forwards none. This prevents undeclared fields (e.g. password inputs) from leaking to unrelated partial endpoints. MicronComposables scroll-mode centering fix: centered/right-aligned lines now use exact viewport width (fillMaxWidth + TextAlign) instead of min-width, so text actually centers rather than left-aligning. Updated MicronParserTest to use a real-world NomadNet node index line with trailing text after a styled link. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lxmf/messenger/nomadnet/PartialManager.kt | 26 ++++++++++++--- .../ui/components/MicronComposables.kt | 33 ++++++++++--------- .../lxmf/messenger/micron/MicronParserTest.kt | 12 ++++--- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt b/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt index f0548fe6d..5c18b4702 100644 --- a/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt +++ b/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt @@ -37,6 +37,7 @@ class PartialManager( val status: Status, val document: MicronDocument?, val refreshInterval: Int?, + val fieldNames: List = emptyList(), ) { enum class Status { LOADING, LOADED, ERROR } } @@ -138,6 +139,7 @@ class PartialManager( status = PartialState.Status.LOADING, document = null, refreshInterval = partial.refreshInterval, + fieldNames = partial.fieldNames, ) ) } @@ -174,7 +176,7 @@ class PartialManager( jobs[key] = scope.launch(Dispatchers.IO) { - fetchAndUpdate(key, existing.url, existing.refreshInterval) + fetchAndUpdate(key, existing.url, existing.refreshInterval, existing.fieldNames) } } @@ -191,13 +193,14 @@ class PartialManager( key: String, partial: MicronElement.Partial, ) { - fetchAndUpdate(key, partial.url, partial.refreshInterval) + fetchAndUpdate(key, partial.url, partial.refreshInterval, partial.fieldNames) } private suspend fun fetchAndUpdate( key: String, url: String, refreshInterval: Int?, + allowedFields: List = emptyList(), ) { var consecutiveErrors = 0 try { @@ -205,7 +208,7 @@ class PartialManager( fetchSemaphore.withPermit { val (nodeHash, path) = resolveNomadNetUrl(url, currentNodeHash()) - val formDataJson = buildFormDataJson() + val formDataJson = buildFormDataJson(allowedFields) val result = protocol.requestNomadnetPage( @@ -273,17 +276,30 @@ class PartialManager( status = PartialState.Status.ERROR, document = null, refreshInterval = refreshInterval, + fieldNames = allowedFields, ) ) } } } - private fun buildFormDataJson(): String? { + private fun buildFormDataJson(allowedFields: List): String? { val fields = formFields() if (fields.isEmpty()) return null + // Match NomadNet TUI: only forward fields declared by the partial. + // "*" means all fields; empty list means no fields. + val allFields = "*" in allowedFields + val filtered = + if (allFields) { + fields + } else if (allowedFields.isEmpty()) { + return null + } else { + fields.filterKeys { it in allowedFields } + } + if (filtered.isEmpty()) return null val json = JSONObject() - for ((k, v) in fields) { + for ((k, v) in filtered) { json.put(k, v) } return json.toString() diff --git a/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt b/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt index 386127017..dc96326ce 100644 --- a/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt +++ b/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt @@ -207,20 +207,21 @@ private fun MicronLineComposable( val indentPadding = (line.indentLevel * INDENT_DP).dp - // In scroll mode (MONOSPACE_SCROLL), use min-width for ALL alignments so wide - // lines (e.g. ASCII art) can extend beyond the viewport and scroll horizontally. - // In other modes, centered/right-aligned lines use exact viewport width so text - // wraps within the viewport (matching NomadNet terminal behavior). + // In scroll mode, left-aligned lines use min-width so wide content (ASCII art) + // can extend beyond the viewport and scroll horizontally. Centered/right-aligned + // lines use exact viewport width so text wraps and TextAlign actually centers. val isScrollMode = renderingMode == RenderingMode.MONOSPACE_SCROLL + val scrollAligned = isScrollMode && line.alignment != MicronAlignment.LEFT val widthModifier = if (minLineWidth != Dp.Unspecified) { - if (isScrollMode) { - Modifier.widthIn(min = minLineWidth) - } else { - when (line.alignment) { - MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth) - MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth) - } + when { + scrollAligned -> Modifier.width(minLineWidth) + isScrollMode -> Modifier.widthIn(min = minLineWidth) + else -> + when (line.alignment) { + MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth) + MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth) + } } } else { Modifier @@ -347,11 +348,11 @@ private fun MicronLineComposable( val lineModifier = widthModifier - .then(if (isScrollMode) Modifier else Modifier.fillMaxWidth()) + .then(if (isScrollMode && !scrollAligned) Modifier else Modifier.fillMaxWidth()) .padding(start = indentPadding) .then(if (headingBg != null) Modifier.background(headingBg) else Modifier) .then( - if (hasLinks && !isScrollMode) { + if (hasLinks && (!isScrollMode || scrollAligned)) { Modifier.defaultMinSize(minHeight = MIN_LINK_HEIGHT_DP.dp) } else { Modifier @@ -386,11 +387,13 @@ private fun MicronLineComposable( ) } + val softWrap = !isScrollMode || scrollAligned + if (hasLinks) { ClickableText( text = annotatedString, modifier = lineModifier, - softWrap = !isScrollMode, + softWrap = softWrap, style = textStyle, onClick = { offset -> annotatedString @@ -409,7 +412,7 @@ private fun MicronLineComposable( Text( text = annotatedString, modifier = lineModifier, - softWrap = !isScrollMode, + softWrap = softWrap, style = textStyle, ) } diff --git a/micron/src/test/java/com/lxmf/messenger/micron/MicronParserTest.kt b/micron/src/test/java/com/lxmf/messenger/micron/MicronParserTest.kt index 10f847dc5..82d8dc87e 100644 --- a/micron/src/test/java/com/lxmf/messenger/micron/MicronParserTest.kt +++ b/micron/src/test/java/com/lxmf/messenger/micron/MicronParserTest.kt @@ -726,18 +726,20 @@ class MicronParserTest { @Test fun `backtick before link with formatting`() { - // Real-world pattern: `c`F0ad`_`[label`dest]`_`f - val doc = MicronParser.parse("`c`F0ad`_`[\"Hoard's Heart\"`/page/hoardsheart.mu]`_`f") + // Real-world line from a NomadNet node index page + val doc = MicronParser.parse("`c`F0ad`_`[\"Hoard's Heart\" by CupsofJade`:/page/hoardsheart.mu]`_`f - Reticulumified!") assertEquals(MicronAlignment.CENTER, doc.lines[0].alignment) val link = doc.lines[0] .elements .filterIsInstance() .first() - assertEquals("\"Hoard's Heart\"", link.label) - assertEquals("/page/hoardsheart.mu", link.destination) - // No stray backtick in any text element + assertEquals("\"Hoard's Heart\" by CupsofJade", link.label) + assertEquals(":/page/hoardsheart.mu", link.destination) + // Trailing text after the link val texts = doc.lines[0].elements.filterIsInstance() + assertTrue(texts.any { it.content.contains("Reticulumified!") }) + // No stray backtick in any text element assertTrue(texts.none { it.content.contains("`") }) } From e502ab11c72b142b7bd2a5d699e54799d6b880f8 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Fri, 13 Mar 2026 18:32:04 -0400 Subject: [PATCH 2/6] fix: retry link establishment and revert scroll-mode centering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link establishment now retries up to 3 times when the single RNS link request packet is lost (same single-shot-no-retry issue as request_path). Each attempt waits up to the per-hop timeout before tearing down and retrying. Destination-closed (0x03) is treated as permanent and not retried. Reverts the scroll-mode centering change from the previous commit — it caused long lines (ASCII art) to word-wrap instead of scrolling horizontally. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/components/MicronComposables.kt | 33 +++-- python/rns_api.py | 114 +++++++++++------- 2 files changed, 86 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt b/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt index dc96326ce..386127017 100644 --- a/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt +++ b/app/src/main/java/com/lxmf/messenger/ui/components/MicronComposables.kt @@ -207,21 +207,20 @@ private fun MicronLineComposable( val indentPadding = (line.indentLevel * INDENT_DP).dp - // In scroll mode, left-aligned lines use min-width so wide content (ASCII art) - // can extend beyond the viewport and scroll horizontally. Centered/right-aligned - // lines use exact viewport width so text wraps and TextAlign actually centers. + // In scroll mode (MONOSPACE_SCROLL), use min-width for ALL alignments so wide + // lines (e.g. ASCII art) can extend beyond the viewport and scroll horizontally. + // In other modes, centered/right-aligned lines use exact viewport width so text + // wraps within the viewport (matching NomadNet terminal behavior). val isScrollMode = renderingMode == RenderingMode.MONOSPACE_SCROLL - val scrollAligned = isScrollMode && line.alignment != MicronAlignment.LEFT val widthModifier = if (minLineWidth != Dp.Unspecified) { - when { - scrollAligned -> Modifier.width(minLineWidth) - isScrollMode -> Modifier.widthIn(min = minLineWidth) - else -> - when (line.alignment) { - MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth) - MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth) - } + if (isScrollMode) { + Modifier.widthIn(min = minLineWidth) + } else { + when (line.alignment) { + MicronAlignment.CENTER, MicronAlignment.RIGHT -> Modifier.width(minLineWidth) + MicronAlignment.LEFT -> Modifier.widthIn(min = minLineWidth) + } } } else { Modifier @@ -348,11 +347,11 @@ private fun MicronLineComposable( val lineModifier = widthModifier - .then(if (isScrollMode && !scrollAligned) Modifier else Modifier.fillMaxWidth()) + .then(if (isScrollMode) Modifier else Modifier.fillMaxWidth()) .padding(start = indentPadding) .then(if (headingBg != null) Modifier.background(headingBg) else Modifier) .then( - if (hasLinks && (!isScrollMode || scrollAligned)) { + if (hasLinks && !isScrollMode) { Modifier.defaultMinSize(minHeight = MIN_LINK_HEIGHT_DP.dp) } else { Modifier @@ -387,13 +386,11 @@ private fun MicronLineComposable( ) } - val softWrap = !isScrollMode || scrollAligned - if (hasLinks) { ClickableText( text = annotatedString, modifier = lineModifier, - softWrap = softWrap, + softWrap = !isScrollMode, style = textStyle, onClick = { offset -> annotatedString @@ -412,7 +409,7 @@ private fun MicronLineComposable( Text( text = annotatedString, modifier = lineModifier, - softWrap = softWrap, + softWrap = !isScrollMode, style = textStyle, ) } diff --git a/python/rns_api.py b/python/rns_api.py index 0eebd87f0..b5ddb583a 100644 --- a/python/rns_api.py +++ b/python/rns_api.py @@ -226,63 +226,91 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): log_warning("RnsApi", "request_nomadnet_page", f"Destination hash mismatch! passed={dest_hash_hex} computed={node_dest.hash.hex()}") - # ── Phase 4: Establish link ── - log_info("RnsApi", "request_nomadnet_page", - f"Creating link to {dest_hash_hex[:16]} (hops={hops})") + # ── Phase 4: Establish link with retry ── + # RNS sends ONE link request packet with no retry. At high hop + # counts the packet (or its proof) can be lost, leaving the link + # stuck at PENDING until establishment_timeout (~6s × hops). + # Retry up to MAX_LINK_ATTEMPTS times, tearing down the stale + # link before each retry so RNS allocates a fresh request. + MAX_LINK_ATTEMPTS = 3 + # Per-attempt timeout: use the RNS establishment_timeout for the + # hop count, but cap so we leave time for the page request itself. + per_attempt_base = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT * max(1, hops) + 6 + last_reason = None + last_status = None + + for attempt in range(1, MAX_LINK_ATTEMPTS + 1): + if self._cancel_flag: + return {"success": False, "error": "Cancelled"} - link_established = threading.Event() - link_closed_reason = [None] + remaining = deadline - time.time() + if remaining < 5: + break - def on_link_established(established_link): - log_info("RnsApi", "request_nomadnet_page", - f"Link established to {dest_hash_hex[:16]} (RTT={established_link.rtt})") - link_established.set() + if attempt > 1: + log_info("RnsApi", "request_nomadnet_page", + f"Link attempt #{attempt} to {dest_hash_hex[:16]}") + else: + log_info("RnsApi", "request_nomadnet_page", + f"Creating link to {dest_hash_hex[:16]} (hops={hops})") - def on_link_closed(closed_link): - reason = getattr(closed_link, 'teardown_reason', None) - reason_str = {0x01: "TIMEOUT", 0x02: "INITIATOR_CLOSED", 0x03: "DESTINATION_CLOSED"}.get(reason, str(reason)) - log_warning("RnsApi", "request_nomadnet_page", - f"Link to {dest_hash_hex[:16]} closed during establishment (reason={reason_str}, status={closed_link.status})") - link_closed_reason[0] = reason - link_established.set() + link_established = threading.Event() + link_closed_reason = [None] - link = RNS.Link(node_dest, - established_callback=on_link_established, - closed_callback=on_link_closed) + def on_link_established(established_link): + log_info("RnsApi", "request_nomadnet_page", + f"Link established to {dest_hash_hex[:16]} (RTT={established_link.rtt})") + link_established.set() + + def on_link_closed(closed_link): + reason = getattr(closed_link, 'teardown_reason', None) + reason_str = {0x01: "TIMEOUT", 0x02: "INITIATOR_CLOSED", + 0x03: "DESTINATION_CLOSED"}.get(reason, str(reason)) + log_warning("RnsApi", "request_nomadnet_page", + f"Link to {dest_hash_hex[:16]} closed during establishment " + f"(reason={reason_str}, status={closed_link.status})") + link_closed_reason[0] = reason + link_established.set() + + link = RNS.Link(node_dest, + established_callback=on_link_established, + closed_callback=on_link_closed) + + # Wait up to per_attempt_base, but never exceed remaining - 5s + # (reserve 5s for the page request on the last attempt). + attempt_wait = min(per_attempt_base, remaining - 5) + log_debug("RnsApi", "request_nomadnet_page", + f"Waiting up to {attempt_wait:.0f}s for link to {dest_hash_hex[:16]}") + link_established.wait(timeout=max(attempt_wait, 5.0)) - est_timeout = getattr(link, 'establishment_timeout', None) - log_info("RnsApi", "request_nomadnet_page", - f"Link establishment_timeout={est_timeout:.1f}s" if est_timeout else "Link establishment_timeout=unknown") + if self._cancel_flag: + try: + link.teardown() + except Exception: + pass + return {"success": False, "error": "Cancelled"} - link_wait = max(deadline - time.time() - 10, 5.0) - log_debug("RnsApi", "request_nomadnet_page", - f"Waiting up to {link_wait:.0f}s for link to {dest_hash_hex[:16]}") - link_established.wait(timeout=link_wait) + if link.status == RNS.Link.ACTIVE: + return link - if self._cancel_flag: + # Link didn't establish — record reason and tear down + last_reason = link_closed_reason[0] + last_status = str(link.status) try: link.teardown() except Exception: pass - return {"success": False, "error": "Cancelled"} - if link_closed_reason[0] is not None or link.status != RNS.Link.ACTIVE: - status_str = str(link.status) if link else "unknown" - reason = link_closed_reason[0] - log_warning("RnsApi", "request_nomadnet_page", - f"Link to {dest_hash_hex[:16]} failed (status={status_str}, teardown_reason={reason})") - try: - link.teardown() - except Exception: - pass - if reason == 0x03: + # If destination explicitly rejected, don't retry + if last_reason == 0x03: return {"success": False, "error": "Connection closed by node"} - elif reason == 0x01 or link.status == RNS.Link.PENDING: - return {"success": False, "error": f"Connection timed out ({hops} hops). Node may be offline or unreachable."} - else: - return {"success": False, "error": f"Connection failed (status={status_str})"} - return link + # All attempts exhausted + log_warning("RnsApi", "request_nomadnet_page", + f"Link to {dest_hash_hex[:16]} failed after {MAX_LINK_ATTEMPTS} attempts " + f"(last_status={last_status}, last_reason={last_reason})") + return {"success": False, + "error": f"Connection timed out ({hops} hops). Node may be offline or unreachable."} def _send_page_request(self, link, path, request_data, dest_hash_hex, deadline): """Send a page request over an established link. From 5e621c677189f5a45c8a6f500d1affc6d7f738b5 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Fri, 13 Mar 2026 18:44:45 -0400 Subject: [PATCH 3/6] fix: preserve fieldNames in partial state and fix closure capture Address Greptile review findings: - PartialManager: preserve fieldNames in onSuccess and onFailure PartialState constructors so reloads retain field filtering. Refactor buildFormDataJson to use when-expression, reducing return count from 4 to 3 (fixes detekt ReturnCount). - rns_api.py: capture link_established and link_closed_reason via default args in retry loop closures to prevent late-firing callbacks from a prior iteration corrupting the next iteration's state. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/lxmf/messenger/nomadnet/PartialManager.kt | 13 ++++++------- python/rns_api.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt b/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt index 5c18b4702..3172aa7a2 100644 --- a/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt +++ b/app/src/main/java/com/lxmf/messenger/nomadnet/PartialManager.kt @@ -231,6 +231,7 @@ class PartialManager( status = PartialState.Status.LOADED, document = doc, refreshInterval = refreshInterval, + fieldNames = allowedFields, ) ) } @@ -247,6 +248,7 @@ class PartialManager( status = PartialState.Status.ERROR, document = null, refreshInterval = refreshInterval, + fieldNames = allowedFields, ) ) } @@ -288,14 +290,11 @@ class PartialManager( if (fields.isEmpty()) return null // Match NomadNet TUI: only forward fields declared by the partial. // "*" means all fields; empty list means no fields. - val allFields = "*" in allowedFields val filtered = - if (allFields) { - fields - } else if (allowedFields.isEmpty()) { - return null - } else { - fields.filterKeys { it in allowedFields } + when { + "*" in allowedFields -> fields + allowedFields.isEmpty() -> emptyMap() + else -> fields.filterKeys { it in allowedFields } } if (filtered.isEmpty()) return null val json = JSONObject() diff --git a/python/rns_api.py b/python/rns_api.py index b5ddb583a..630c53068 100644 --- a/python/rns_api.py +++ b/python/rns_api.py @@ -257,20 +257,23 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): link_established = threading.Event() link_closed_reason = [None] - def on_link_established(established_link): + # Capture loop variables via default args to avoid late-binding + # closure bug: stale callbacks from a prior iteration could + # otherwise set the new iteration's Event or reason list. + def on_link_established(established_link, _evt=link_established): log_info("RnsApi", "request_nomadnet_page", f"Link established to {dest_hash_hex[:16]} (RTT={established_link.rtt})") - link_established.set() + _evt.set() - def on_link_closed(closed_link): + def on_link_closed(closed_link, _evt=link_established, _reason=link_closed_reason): reason = getattr(closed_link, 'teardown_reason', None) reason_str = {0x01: "TIMEOUT", 0x02: "INITIATOR_CLOSED", 0x03: "DESTINATION_CLOSED"}.get(reason, str(reason)) log_warning("RnsApi", "request_nomadnet_page", f"Link to {dest_hash_hex[:16]} closed during establishment " f"(reason={reason_str}, status={closed_link.status})") - link_closed_reason[0] = reason - link_established.set() + _reason[0] = reason + _evt.set() link = RNS.Link(node_dest, established_callback=on_link_established, From 8e06e5ef652edb801eba0d662b0b86e90ae1fcec Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:20:35 -0400 Subject: [PATCH 4/6] =?UTF-8?q?chore(greptile):=20iteration=201=20?= =?UTF-8?q?=E2=80=94=20applied=201,=20rejected=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- python/rns_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/rns_api.py b/python/rns_api.py index 630c53068..b8847912d 100644 --- a/python/rns_api.py +++ b/python/rns_api.py @@ -238,6 +238,7 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): per_attempt_base = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT * max(1, hops) + 6 last_reason = None last_status = None + attempts_made = 0 for attempt in range(1, MAX_LINK_ATTEMPTS + 1): if self._cancel_flag: @@ -247,6 +248,7 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): if remaining < 5: break + attempts_made += 1 if attempt > 1: log_info("RnsApi", "request_nomadnet_page", f"Link attempt #{attempt} to {dest_hash_hex[:16]}") @@ -308,9 +310,9 @@ def on_link_closed(closed_link, _evt=link_established, _reason=link_closed_reaso if last_reason == 0x03: return {"success": False, "error": "Connection closed by node"} - # All attempts exhausted + # All attempts exhausted (or skipped because deadline was already near-exhausted) log_warning("RnsApi", "request_nomadnet_page", - f"Link to {dest_hash_hex[:16]} failed after {MAX_LINK_ATTEMPTS} attempts " + f"Link to {dest_hash_hex[:16]} failed after {attempts_made} attempt(s) " f"(last_status={last_status}, last_reason={last_reason})") return {"success": False, "error": f"Connection timed out ({hops} hops). Node may be offline or unreachable."} From 42f96bb9039e66acfce05455da4648852f7cf5fe Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:27:53 -0400 Subject: [PATCH 5/6] =?UTF-8?q?chore(greptile):=20iteration=202=20?= =?UTF-8?q?=E2=80=94=20applied=201,=20rejected=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- python/rns_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/rns_api.py b/python/rns_api.py index b8847912d..d1c3a7482 100644 --- a/python/rns_api.py +++ b/python/rns_api.py @@ -235,7 +235,10 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): MAX_LINK_ATTEMPTS = 3 # Per-attempt timeout: use the RNS establishment_timeout for the # hop count, but cap so we leave time for the page request itself. - per_attempt_base = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT * max(1, hops) + 6 + # RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP is the documented Link-level + # alias for RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT (both = 6 in the + # reference; see RNS/Link.py:75 and RNS/Reticulum.py:141). + per_attempt_base = RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, hops) + 6 last_reason = None last_status = None attempts_made = 0 From f6ab866b0f73f87f4880659e7357f4040bc6e3d4 Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:32:27 -0400 Subject: [PATCH 6/6] =?UTF-8?q?chore(greptile):=20iteration=203=20?= =?UTF-8?q?=E2=80=94=20applied=201,=20rejected=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- python/rns_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/rns_api.py b/python/rns_api.py index d1c3a7482..b850c12f5 100644 --- a/python/rns_api.py +++ b/python/rns_api.py @@ -237,8 +237,14 @@ def _establish_link(self, wrapper, dest_hash, dest_hash_hex, deadline): # hop count, but cap so we leave time for the page request itself. # RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP is the documented Link-level # alias for RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT (both = 6 in the - # reference; see RNS/Link.py:75 and RNS/Reticulum.py:141). - per_attempt_base = RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, hops) + 6 + # reference; see RNS/Link.py:75 and RNS/Reticulum.py:141). Use a + # defensive getattr chain so older/fork RNS builds that don't expose + # the constant under either name fall back to the literal default of + # 6s rather than raising AttributeError and breaking all browsing. + per_hop_timeout = getattr( + RNS.Link, 'ESTABLISHMENT_TIMEOUT_PER_HOP', + getattr(RNS.Reticulum, 'DEFAULT_PER_HOP_TIMEOUT', 6)) + per_attempt_base = per_hop_timeout * max(1, hops) + 6 last_reason = None last_status = None attempts_made = 0