fix(voice-mode): make Termux:API detection robust against pm probe failures

\`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
This commit is contained in:
xxxigm
2026-05-23 23:14:39 +07:00
committed by Teknium
parent 0560f52047
commit e3f4dc2103
+64 -11
View File
@@ -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: