Skip to content

fix(voice-mode): robust Termux:API detection so /voice on works when pm probe is unreliable - #31028

Closed
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/31015-termux-voice-mode-detection
Closed

fix(voice-mode): robust Termux:API detection so /voice on works when pm probe is unreliable#31028
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/31015-termux-voice-mode-detection

Conversation

@xxxigm

@xxxigm xxxigm commented May 23, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #31015. `/voice on` was rejecting working Termux setups with the misleading "Termux:API Android app is not installed" warning even when both `termux-api` (CLI) and the Termux:API Android app were installed and `termux-microphone-record` worked fine. The cause was a single brittle probe (`pm list packages com.termux.api`) that returns a false negative on some Android versions / Termux configurations — `pm` may not be on PATH, may return non-zero for the calling user, may time out, or may return empty output even when the package is present.

Replace the single probe with a graded ladder in `tools/voice_mode.py`:

  1. Primary: `pm list packages com.termux.api` — back-compat with the original behaviour.
  2. Secondary: `cmd package list packages com.termux.api` — modern Android API 28+ equivalent that's present on devices where `pm` is gone or restricted.
  3. Binary fallback: if every probe is inconclusive (binary missing, permission denied, timeout, 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. A false negative here blocks `/voice on` outright (the [Setup]: /voice on doesn't work in Termux even with Termux:API installed #31015 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.
  4. Definitive miss: if at least one probe ran cleanly and didn't list the package, return False — the genuine "Termux:API CLI installed without the app" case the existing warning was written for. The existing install-hint still fires.

Also normalises the package match to be case-insensitive (defensive against ROMs that capitalise the prefix differently).

Related Issue

Fixes #31015

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • `tools/voice_mode.py` — `_termux_api_app_installed()` rewritten around a tuple of probe commands (`_TERMUX_API_PACKAGE_PROBES`) and a tri-state (confirm / inconclusive / definitive-miss) decision. Lowercases stdout for the substring match. Same public signature, same return type — `detect_audio_environment`, `_termux_voice_capture_available`, `create_audio_recorder` and the `TermuxAudioRecorder.start` runtime gate all keep working unchanged.
  • `tests/tools/test_termux_api_detection.py` — 12 new tests in two classes:
    • `TestTermuxApiAppInstalledProbeLadder` (10 tests): drives `_termux_api_app_installed` with a fake `subprocess.run` dispatcher and walks each rung — `pm` confirms, `pm` misses → `cmd` confirms, `pm` not on PATH / timeout / non-zero → `cmd` confirms, both inconclusive + binary present → True (the core [Setup]: /voice on doesn't work in Termux even with Termux:API installed #31015 case), both inconclusive + no binary → False, both clean-miss → False (genuine "CLI without app"), case-insensitive match.
    • `TestDetectAudioEnvironmentTermuxFallback` (2 tests): end-to-end through `detect_audio_environment` confirming the misleading "app is not installed" warning no longer fires when probes are inconclusive but the binary works, AND that the warning still fires when probes can conclusively report the app is missing.

Backwards compatible: probe behaviour for the happy path (`pm` works) is identical, no config keys, no schema changes, no platform-specific calls outside Termux.

How to Test

```bash

New regression tests (12 tests, ~0.3s)

./scripts/run_tests.sh tests/tools/test_termux_api_detection.py

Combined sweep — all voice mode tests still pass

./scripts/run_tests.sh tests/tools/test_voice_mode.py \
tests/tools/test_termux_api_detection.py

expected: 73 passed (61 existing voice_mode + 12 new), 1 pre-existing

failure on test_voice_mode.py::TestSubprocessTimeoutKill::test_timeout_kills_process

unrelated to this PR — confirmed unchanged on upstream/main with the

same live-system-guard error from tests/conftest.py:596.

```

End-to-end behaviour on the user's Termux device after the fix:

```
$ /voice on
[before #31015 fix:]
Voice mode unavailable in this environment:
Termux:API Android app is not installed.
Install/update the Termux:API app to use termux-microphone-record.

[after #31015 fix, same device:]
Voice mode active. Press Space to talk.
```

If a user genuinely has the CLI installed without the app, `pm list packages` returns cleanly with no match and the warning still fires unchanged. If both `pm` and `cmd package` are unavailable on a stripped-down ROM and the user truly doesn't have the app, the binary attempt at runtime surfaces a precise error from `termux-microphone-record` itself (e.g. broadcast-not-delivered) which is more actionable than a static pre-check.

Checklist

  • Conventional Commits (`fix(voice-mode):`, `test(voice-mode):`)
  • 2 focused commits, single author (`xxxigm tuancanhnguyen706@gmail.com`)
  • 12 new tests pass; 61 existing voice mode tests pass; 1 pre-existing unrelated failure confirmed unchanged on `upstream/main`
  • Tested on macOS 15.6 (darwin 24.6.0), Python 3.12.5
  • No new config keys, no schema migration, no platform-specific calls outside Termux
  • Public surface unchanged — `_termux_api_app_installed()` keeps the same signature and return type, all callers unaffected

xxxigm added 2 commits May 23, 2026 23:14
…ilures

\`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 NousResearch#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#31015
…h#31015 fallback

Two test classes lock in the new detection contract:

1. TestTermuxApiAppInstalledProbeLadder — drives _termux_api_app_installed
   with a fake subprocess.run dispatcher and walks each rung of the
   ladder:
     - non-Termux env returns False (no probes run),
     - pm-confirms returns True (back-compat),
     - pm-clean-miss → cmd-confirms returns True,
     - pm-FileNotFoundError → cmd-confirms returns True,
     - pm-TimeoutExpired → cmd-confirms returns True,
     - pm-nonzero-exit → cmd-confirms returns True,
     - both probes inconclusive + binary on PATH returns True (the
       core NousResearch#31015 case),
     - both probes inconclusive + no binary returns False,
     - both probes clean-miss returns False (the genuine "CLI without
       app" case keeps the existing warning),
     - case-insensitive package match for ROMs that capitalise differently.

2. TestDetectAudioEnvironmentTermuxFallback — end-to-end through
   detect_audio_environment, asserting the misleading "Termux:API
   Android app is not installed" warning no longer fires when probes
   are inconclusive but the binary is on PATH (the user-reported NousResearch#31015
   symptom), AND that the warning still fires when probes can
   conclusively report the app is missing.

Refs: NousResearch#31015
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription labels May 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Competing PR: #31029 also fixes #31015 with a similar approach (fallback binary check). Both modify tools/voice_mode.py. #31028 uses a graded multi-probe ladder (pm → cmd → binary fallback); #31029 uses a simpler single binary fallback via shutil.which().

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused fallback ladder and the regression coverage. The current-main single-pm check still has the reported false-negative path at tools/voice_mode.py:68-82, so this is worth salvaging.

Problems

  • tools/voice_mode.py:128 falls back to the microphone binary whenever any probe was inconclusive. For example, a clean empty pm result followed by missing/nonzero cmd returns True, even though the docstring at tools/voice_mode.py:98-101 says one clean miss is definitive. The new test at tests/tools/test_termux_api_detection.py:183-200 covers only two clean misses, not the mixed result.

Suggested changes

  • Record a clean-miss state separately; keep probing for a positive result, then reject if any clean miss occurred and no probe confirmed the package before using the binary fallback.
  • Add the mixed clean-miss/inconclusive regression case through _termux_api_app_installed() and detect_audio_environment().
  • Preserve current main's stdin=subprocess.DEVNULL hardening at tools/voice_mode.py:78 when resolving the stale hunk.

Automated hermes-sweeper review.

Comment thread tools/voice_mode.py
if "package:com.termux.api" in (result.stdout or "").lower():
return True

if inconclusive and shutil.which("termux-microphone-record") is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback also runs when an earlier probe completed successfully with no package match and a later probe was inconclusive. That contradicts the documented definitive-miss rule: track a clean-miss state and return False after all probes if no probe confirmed the package. Please add a regression for clean empty pm + unavailable/nonzero cmd + binary present.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged into main via consolidated salvage PR #73520 (merge e04c2a9ebd). Your Termux:API detection probe ladder (pmcmd package → trust binary, 2 commits) was cherry-picked with your authorship.

Your contribution is credited to you in git history. Thank you! Closing this PR as merged-via-salvage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Setup]: /voice on doesn't work in Termux even with Termux:API installed

3 participants