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

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: