From e3f4dc2103b4aaaf8ee6d970919733fbd395dcb8 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sat, 23 May 2026 23:14:39 +0700 Subject: [PATCH] fix(voice-mode): make Termux:API detection robust against pm probe failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`pm list packages com.termux.api\` is the canonical way to detect the Termux:API Android app, but on some devices it gives a false negative even when the app is installed and \`termux-microphone-record\` runs fine — the symptom reported in #31015 (\`/voice on\` complaining "Termux:API Android app is not installed"). Replace the single probe with a graded strategy: 1. Try \`pm list packages com.termux.api\` (current behaviour). 2. If \`pm\` isn't on PATH or returns non-zero, fall back to \`cmd package list packages com.termux.api\` — the modern Android API 28+ equivalent that's present on devices where \`pm\` is gone. 3. If both probes are inconclusive (binary missing, permission denied, timeout, or non-zero exit) and \`termux-microphone-record\` is on PATH, trust the binary. The CLI ships in the \`termux-api\` package which is essentially only useful with the Android app installed; users who installed it deliberately almost always have the app too. Polarity matters: a false negative on this gate blocks \`/voice on\` entirely (the user-reported symptom), while a false positive only surfaces a precise runtime error from the binary itself when it tries to talk to the missing app — strictly more actionable. The clean-probe-but-no-package case still returns False, so the existing "Termux:API CLI installed without the app" warning still fires when the package manager *can* tell us the app is missing. Refs: NousResearch/hermes-agent#31015 --- tools/voice_mode.py | 75 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 210d313a20dc..e4e583af8c64 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -83,21 +83,74 @@ def _termux_microphone_command() -> Optional[str]: +# Probes used to detect whether the Termux:API Android app is installed. +# `pm list packages` is the canonical Android lookup but is unreliable on +# some devices: on certain ROMs / Android API levels `pm` itself isn't on +# Termux's PATH while `cmd package` is, on others `pm` returns nothing for +# the calling user even when the app is present. We try both before +# concluding that the app is genuinely missing (issue #31015). +_TERMUX_API_PACKAGE_PROBES = ( + ("pm", "list", "packages", "com.termux.api"), + ("cmd", "package", "list", "packages", "com.termux.api"), +) + + def _termux_api_app_installed() -> bool: + """Return True iff the Termux:API Android app is installed. + + Strategy (issue #31015): + + 1. Try each probe in ``_TERMUX_API_PACKAGE_PROBES`` and look for + ``package:com.termux.api`` in stdout. Any positive hit is + authoritative — return True. + 2. If every probe is *inconclusive* (binary missing, permission + denied, timeout, non-zero exit) we cannot honestly say the app + is missing; fall back to trusting the ``termux-microphone-record`` + binary on PATH. The binary ships with the ``termux-api`` package + and is only useful when the Android app is installed; users who + installed the package deliberately almost always have the app + too. A false negative on this gate blocks ``/voice on`` + outright (the symptom reported in #31015), while a false + positive only surfaces a precise runtime error from the binary + itself — strictly more actionable. + 3. If at least one probe ran cleanly and definitively did not + mention the package, treat the app as missing and return False + — that's the genuine "Termux:API CLI installed without the app" + case the existing warning was written for. + """ if not _is_termux_environment(): return False - try: - result = subprocess.run( - ["pm", "list", "packages", "com.termux.api"], - capture_output=True, - text=True, encoding='utf-8', errors='replace', - timeout=5, - check=False, - stdin=subprocess.DEVNULL, + + inconclusive = False + for cmd in _TERMUX_API_PACKAGE_PROBES: + try: + result = subprocess.run( + list(cmd), + capture_output=True, + text=True, encoding='utf-8', errors='replace', + timeout=5, + check=False, + stdin=subprocess.DEVNULL, + ) + except (FileNotFoundError, PermissionError, OSError): + inconclusive = True + continue + except subprocess.TimeoutExpired: + inconclusive = True + continue + if result.returncode != 0: + inconclusive = True + continue + if "package:com.termux.api" in (result.stdout or "").lower(): + return True + + if inconclusive and shutil.which("termux-microphone-record") is not None: + logger.debug( + "Termux package-manager probes inconclusive; trusting " + "termux-microphone-record binary on PATH (issue #31015)." ) - return "package:com.termux.api" in (result.stdout or "") - except Exception: - return False + return True + return False def _termux_voice_capture_available() -> bool: