Skip to content

fix(desktop): resolve a Hermes-capable interpreter for the .desktop Exec - #92122

Open
autumn8-builds wants to merge 2 commits into
NousResearch:mainfrom
autumn8-builds:fix/desktop-entry-venv-wrapper-v2
Open

autumn8-builds wants to merge 2 commits into
NousResearch:mainfrom
autumn8-builds:fix/desktop-entry-venv-wrapper-v2

Conversation

@autumn8-builds

@autumn8-builds autumn8-builds commented Aug 22, 2026 •

Copy link
Copy Markdown

Summary

Fixes the Linux .desktop launcher failing silently when Hermes is launched via an interpreter that cannot import hermes_cli (e.g. a uv-managed shim, or a venv bin/python whose symlink is followed out of the venv). Closes #92086.

resolve_exec_command() previously picked the interpreter from sys.executable (via Path(...).resolve(), which follows the venv symlink onto the base interpreter that lacks Hermes' deps) and blindly prefixed it. The generated entry then dies on import hermes_cli — invisibly, because Terminal=false. Clicking the pinned launcher does nothing.

Fix

The prefix interpreter is now chosen by capability, not by resolve():

  1. sys.executable — but only if it can import hermes_cli
  2. the installed venv-backed wrapper on PATH (e.g. ~/.local/bin/hermes), gated on it being genuinely runnable (a native binary or bash launcher, not a foreign python3 shebang script that would reproduce the broken form)
  3. sys.executable -m hermes_cli.main

A venv's bin/python is a symlink to the base interpreter on POSIX (python -m venv and uv venv both do this), so the capable-interpreter probe uses os.path.abspath(sys.executable) rather than Path(...).resolve() — abspath normalises without dereferencing the symlink, keeping the selection inside the venv. The _needs_interpreter comparison is case-folded so a venv path with an uppercase segment isn't misclassified as foreign.

The capability probe runs in isolated mode (-I -c "import hermes_cli.main", cwd=/) so an inherited PYTHONPATH cannot fake the import gate (hardening from the #88709 reporter, @nosliwhtes).

StartupNotify=false (separate change, called out per review)

render_desktop_entry() sets StartupNotify=false. The Electron startup token does not propagate through the wrapper, so true leaves a pinned launcher spinning forever. Window association still works via StartupWMClass=Hermes.

Test plan

  • tests/hermes_cli/test_linux_desktop_entry.py: 21 passed, 2 platform-skipped.
  • Covers: venv-backed PATH wrapper wins when a bare launcher's interpreter can't import hermes_cli; falls back to sys.executable -m hermes_cli.main when no wrapper exists; venv python with an uppercase path segment recognised as in-venv; _needs_interpreter preserves the venv symlink; capability probe rejects a PYTHONPATH-injected checkout false positive.

Notes

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) area/install-update Installer, updater, packaging, wheels, doctor P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades duplicate This issue or pull request already exists labels Aug 22, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Duplicate of #92088. The diffs are byte-identical, including the interpreter-capability fallback, StartupNotify change, and focused tests; #92088 remains the canonical open PR.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.
Both failure modes are real and the fixes are proportionate. Verified python -m hermes_cli.main works (module has a __main__ guard), so the last-resort fallback is viable. The resolution order is right: probe sys.executable for hermes_cli importability first (because under uv/system shims sys.executable itself may lack Hermes' deps — prefixing it would reproduce the silent failure), then the PATH wrapper (which exec's the venv python), then -m. The StartupNotify=false change closes the infinite-spinning-pin symptom, with the rationale (Electron's startup token never crosses the wrapper) documented inline.

Test notes:

  1. Selectively stubbing subprocess.run by sniffing "import hermes_cli" in the args keeps other subprocess calls live — cleaner than a blanket mock, and the captured _orig_run pattern is tidy.
  2. Both new Exec assertions check the full parsed line, including absolute-path requirements on the fallback.

Minor items:

  1. _can_import_hermes_cli uses timeout=30 — generous for an import probe; if a user's interpreter hangs on site initialization, desktop-entry generation stalls up to 30s. Something like 10s would bound the worst case while staying safe on slow disks.
  2. Consider logging which branch won (sys.executable / wrapper / -m) at debug level when generating the entry — the next "dead click" report will be much faster to triage with that line in the log.

@autumn8-builds

Copy link
Copy Markdown
Author

@jackulau — thank you, this is a genuinely better fix and you're right that .resolve() follows the venv symlink out of the box. I've folded your os.path.abspath() change into this PR (kept as the capability ladder, swapped the symlink-following resolution for abspath) plus the case-fold on _needs_interpreter, with tests updated to assert the venv python now stays in Exec=. Verified on a uv-venv host.

You're also right about StartupNotify=false being a separate behavioural change — I've left it in here with its own line in the PR body rather than silently riding along, per your note.

Re: landing — I'm happy for this PR to be the base and for you to port _running_interpreter() and the _needs_interpreter fix onto it if the maintainers prefer that split; either way the combined fix is what matters. Appreciate you putting the overlap in front of me instead of letting two PRs race.

@alt-glitch alt-glitch removed the duplicate This issue or pull request already exists label Aug 23, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #92088 is a closed, unmerged predecessor for the same capability-gated launcher repair. #92122 remains an active resubmission and should not retain a duplicate disposition; it also competes with the distinct symlink-preservation approach in #92090.

@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch from 90a8baf to fe4ac25 Compare August 23, 2026 01:31
@autumn8-builds

Copy link
Copy Markdown
Author

@Enough1122 (and maintainers) — both minor review notes were good calls, applied in the latest force-push:

  1. _can_import_hermes_cli probe timeout lowered 30s → 10s, so a hung interpreter on site-init can't stall desktop-entry generation.
  2. Added debug-level logging in _interpreter_for noting which branch won (sys.executable / PATH wrapper / -m), so the next "dead click" report is faster to triage.

No behaviour change to the resolution order, Exec output, or StartupNotify=false. Tests still green.

(Noted triage's duplicate label was already withdrawn — #92122 remains the active resubmission; #92088 is the closed predecessor.)

@jackulau

Copy link
Copy Markdown

Thanks for the cross-post on #92090, and for folding in the abspath reasoning
rather than just taking the line. Agreed on not racing — I have no attachment to
which number lands, and I would rather review yours properly than defend mine.

So, one thing I think is a real defect, in _interpreter_for fallback 2:

wrapper = shutil.which("hermes")
if wrapper:
    argv = [str(Path(wrapper).resolve()), "desktop"]

That rung is only reached when _can_import_hermes_cli(sys.executable) came back
False. Its docstring justifies it as "the installed hermes wrapper on PATH,
which exec's the project venv python", but nothing checks that, and in the
reporter's own configuration it is false. Trace it:

  • resolve_exec_command gets bin_path from resolve_hermes_bin()
    (relaunch.py:80), whose order is argv[0], then shutil.which("hermes"), then
    None.
  • Whenever argv[0] is not a usable executable — module invocation, or a -c
    style argv0 — that function returns shutil.which("hermes") verbatim. So
    resolved and your wrapper are the same file, and fallback 2 hands back
    Path(that).resolve() + ["desktop"]: byte-for-byte the argv the pre-fix code
    produced, and the one _needs_interpreter(resolved) just answered True about
    one frame earlier.
  • In Linux desktop entry generated with non-runnable Exec; icon launch always fails #90292's actual setup it is the same file even when argv[0] is usable,
    because argv[0] is the shell installer's bash wrapper and that wrapper is what
    is on PATH. Both paths resolve to the repo hermes script carrying
    #!/usr/bin/env python3.

The net effect is narrow but bad: on exactly the machines where sys.executable
cannot import hermes_cli — which is the scenario this whole resolution ladder
exists for — the generated Exec= silently reverts to the broken form, and
Terminal=false means the user sees a menu entry that does nothing, same as
before. Fallback 3 would have been correct there.

Two ways out, and I do not have a strong preference: either gate rung 2 on
not _needs_interpreter(Path(wrapper).resolve()), or drop rung 2 entirely and
let it fall to -m hermes_cli.main, which is already the answer for the
no-launcher case and does not depend on a shebang. A test that sets
_can_import_hermes_cli to False with a hermes on PATH whose shebang is
/usr/bin/env python3 would pin whichever you pick; I could not find one in
test_linux_desktop_entry.py that reaches rung 2 at all.

Two smaller notes, both non-blocking:

  • _can_import_hermes_cli spawns a subprocess during entry generation. The
    10s timeout is the right instinct, but import hermes_cli is not free and this
    runs on the install path; -S -c "import importlib.util,sys; sys.exit(importlib.util.find_spec('hermes_cli') is None)" answers the same
    question without executing the package.
  • The probe result is not cached, and _running_interpreter() is called three
    times in _interpreter_for for a value that cannot change. Cosmetic.

On base choice: yours is the larger and more general fix and I am happy for it to
be the one that lands. Say the word once rung 2 is settled and I will close
#92090 pointing here, so the maintainers only have one to read.

@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch from fe4ac25 to d52ffb3 Compare August 23, 2026 05:17
@autumn8-builds

Copy link
Copy Markdown
Author

@jackulau — that rung-2 trace is exactly right and it's a genuine defect; thank you for working through it rather than letting it slide. Applied in the latest force-push (d52ffb3): rung 2 is now gated on not _needs_interpreter(Path(wrapper).resolve()), so a hermes wrapper that is itself a foreign python3 shebang script is skipped and the ladder falls through to -m hermes_cli.main instead of silently reverting to the broken Exec=. Added a regression test (test_exec_does_not_reuse_foreign_python_shebang_wrapper) pinning the case you described — sys.executable fails the capability probe with a /usr/bin/env python3 hermes on PATH → must land on -m, never the wrapper. Full suite: 19 passed, 2 skipped.

On your two smaller notes — I left them as-is deliberately:

  • The -S -c "importlib.util.find_spec" probe: I tested it and it works on a real venv, but it's a behaviour change to the import gate and you flagged it non-blocking, so I kept the proven import hermes_cli with the 10s timeout. Happy to revisit if a maintainer prefers the cheaper probe.
  • The _running_interpreter() caching + triple-call: cosmetic, same; left for a follow-up rather than widen this diff.

Glad to have #92122 as the base. Once you're satisfied rung 2 is settled, close #92090 whenever — this is the one you recommended and I'm happy for it to land.

(Maintainers: this PR now combines the symlink-preserving abspath semantics with the Jack Lau capability ladder, and the rung-2 fix above closes the gap he found where the ladder could silently emit the pre-fix broken form. #92090 can be closed in favour of this.)

@nosliwhtes

nosliwhtes commented Aug 25, 2026 •

Copy link
Copy Markdown

Real hardware confirmation from the machine that produced #88709: Zorin 18.1, Python 3.11.16, uv-managed venv.

  • /home/nosliwhtes/.hermes/hermes-agent/.venv/bin/python can import hermes_cli.main under a minimal desktop-style environment.
  • Resolving that symlink produces /home/nosliwhtes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11, which fails with ModuleNotFoundError under the same environment.
  • Current main generates: /home/nosliwhtes/.local/share/uv/python/cpython-3.11.16-linux-x86_64-gnu/bin/python3.11 /home/nosliwhtes/.hermes/hermes-agent/hermes desktop
  • This PR generates: /home/nosliwhtes/.hermes/hermes-agent/.venv/bin/python /home/nosliwhtes/.hermes/hermes-agent/hermes desktop
  • Fresh focused suites: main 15 passed, 2 skipped; this head 19 passed, 2 skipped.

There are two testable gaps worth fixing before merge.

First, test_exec_leaves_venv_shebang_scripts_alone writes its shebang with Path(sys.executable).resolve(), which produces the base interpreter path. That does not test a real venv shebang. _needs_interpreter() also compares against Path(sys.executable).resolve().parent, so a real shebang containing .venv/bin/python is classified as foreign. The prefixed command still works, but the claimed no-prefix behavior is not proven. Smallest fix: build that test shebang from os.path.abspath(sys.executable), then compare _needs_interpreter() against the non-dereferenced running-interpreter directory.

Second, _can_import_hermes_cli() probes only import hermes_cli and inherits the current working directory. With the base interpreter, that probe succeeds from the repository root because the source tree is importable, while import hermes_cli.main fails immediately because its dependencies are absent. From home or /tmp, even the package import fails. That can falsely certify the exact interpreter this change is meant to reject. Probing import hermes_cli.main from a neutral working directory would pin the real capability.

Ruff check is clean. Ruff format --check reports that both touched files need formatting. The raw checkout case itself is fixed on this machine.

@alt-glitch alt-glitch removed the needs-decision Awaiting maintainer decision before any implementation label Aug 25, 2026
@nosliwhtes

Copy link
Copy Markdown

I pushed the two follow-up corrections to nosliwhtes:fix/linux-desktop-interpreter-fallback-review.

Cherry-pick:

git cherry-pick 4150501f641829a961ae7e0deef538e46f1a395c

This commit is based directly on cadc3aaa and only changes hermes_cli/linux_desktop_entry.py plus its test file. It keeps the non-dereferenced venv path for shebang matching, then probes hermes_cli.main from a neutral cwd under Python -I. That prevents both checkout-root and inherited PYTHONPATH false positives.

Verification:

  • canonical isolated runner: 21 passed, 0 failed, 2 expected OS skips
  • ruff check . and production-file ty check: passed
  • scrubbed Zorin/UV runtime: venv accepted, resolved base interpreter rejected, generated Exec= kept .venv/bin/python
  • disposable full-suite comparison: same 11 pre-existing missing-acp collection errors as untouched cadc3aaa

I did not modify or force-push your branch.

@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch from e865a6a to 63a1d75 Compare September 3, 2026 05:00
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: #92122 is an active, capability-gated repair in the Linux desktop-entry launcher cluster. It overlaps open #92090 and #94051 but has concrete implementation and review deltas, so this is a maintainer selection rather than a duplicate closure.

@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch from 63a1d75 to 1952271 Compare September 3, 2026 11:00
@alt-glitch alt-glitch removed needs-decision Awaiting maintainer decision before any implementation comp/desktop Electron desktop app (apps/desktop/*) labels Sep 3, 2026
@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch 3 times, most recently from 7e217e0 to 1bec735 Compare September 4, 2026 23:00
@alt-glitch alt-glitch added the comp/desktop Electron desktop app (apps/desktop/*) label Sep 4, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Correction to the earlier triage note: #92088 has since been closed, so this PR is the active fix for #92086 / #101097 (not a duplicate). It competes with #94051, which takes the alternate approach of preserving the venv interpreter symlink in Exec=; a maintainer should pick one.

@autumn8-builds
autumn8-builds force-pushed the fix/desktop-entry-venv-wrapper-v2 branch 11 times, most recently from 1057e12 to 1458b99 Compare September 7, 2026 17:00
@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown

exact-head 8de6a65 — void if moved

KEEP: Linux .desktop Exec= resolves a Hermes-capable interpreter instead of following sys.executable out of uv/venv shims (hermes_cli/linux_desktop_entry.py + tests). Fixes silent launcher failure when the desktop entry is regenerated on launch (#92086).

PICK-ONE vs open spray (same file; all CONFLICTING narrower leaves):

Recommendation: land this PR; close or rebase the CONFLICTING spray onto it.

SOFT (different design — do not auto-close): #98381 points Exec= at the packaged Electron binary for fire-and-forget GNOME favorites — rebase/compose after #92122 if maintainers want that path too.

Upstream PR NousResearch#92090 / Nous's own merge (268e105) fixed interpreter
path resolution but left StartupNotify=true, which causes a perpetual
launcher spinner (gtk-launch hangs) when the desktop process exits
before StartupId acknowledgment. Set false to prevent the hang.

On top of NousResearch#92090.
@autumn8-builds

Copy link
Copy Markdown
Author

Ping @jackulau — branch rebased onto main (which now includes the Nous interpreter fix at 268e1055e8), with the StartupNotify=false fix re-applied on top. CI expired for this PR; can you or another Nous maintainer approve the workflow runs so we can get the green check?

Diff is just the one-line StartupNotify=true → StartupNotify=false change on top of main. The interpreter path resolution is already handled by the merged upstream fix.

…h (exit 1002)

When Electron's GPU process terminates abnormally on Linux, the
desktop process exits with code 1002, leaving the user with a
frozen spinner. Detect this specific exit code, set
HERMES_DESKTOP_DISABLE_GPU=1, and retry the launch once — no
manual config change needed.

Complements the configurable desktop.disable_gpu option by adding
the automatic fallback the launcher path was missing.

This branch has not been deployed

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

Labels

area/install-update Installer, updater, packaging, wheels, doctor area/sessions Session lifecycle, resume, persistence, history comp/acp Agent Communication Protocol adapter comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux .desktop launcher fails silently when sys.executable lacks hermes_cli (e.g. uv-managed install)

7 participants