Skip to content

fix(desktop): stop installing a hybrid plugin's desktop half twice - #101046

Closed
ocuclaw wants to merge 1 commit into
NousResearch:mainfrom
ocuclaw:fix/desktop-hybrid-install-skip-dup
Closed

ocuclaw wants to merge 1 commit into
NousResearch:mainfrom
ocuclaw:fix/desktop-hybrid-install-skip-dup

Conversation

@ocuclaw

@ocuclaw ocuclaw commented Sep 2, 2026 •

Copy link
Copy Markdown

What does this PR do?

On a local backend a unified package's desktop half is served from the package copy on every install path, including the already-installed retry. The install flow no longer creates the second on-disk copy from #100412.

What main still did. plugin-install-modal.tsx::handleInstall gates the package path on agentInstalled && desktopHalfFromPackage (main :250). On a no-Force retry of a package already on disk the agent install returns Plugin '<name>' already exists. Use force reinstall (hermes_cli/plugins_cmd.py:789), agentInstalled stays false, and the modal falls through to installDesktopPlugin (main :263), which clones desktop-plugins/<git-name>/ next to the desktop-plugins/<manifest-name>/ copy that reconcileDesktopPlugins() already materialised. Two folders, one plugin id. Since #118902 (e1a6679399) the loader shows the second as an error row instead of running both, but the install flow still creates it whenever the repo's git name differs from its manifest name: (same-name retries are refused by desktop-plugin-install.ts:410-414 and create nothing). The comment at main :176-179 already states the invariant this violates: only a remote backend or a desktop-only repo needs a separate desktop clone.

Case (local backend, hybrid repo, both boxes ticked) Before After
No-Force retry, repo name != manifest name agent error + desktop-plugins/<git-name>/ cloned beside <manifest-name>/ agent error only, one folder
Fresh install, agent clone fails agent error + standalone desktop clone (duplicated by the next successful install) agent error only, nothing left behind
Fresh install, agent ok package half materialised unchanged
Remote backend separate clone unchanged

Related Issue

Fixes #100412 (install-time half; the runtime half landed in #118902). Supersedes the install-side scope of #100485.

Type of Change

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

Changes Made

  • apps/desktop/src/app/settings/plugin-install-modal.tsx::handleInstall: the package path is taken whenever desktopHalfFromPackage holds, whatever the agent install returned; reconcileDesktopPlugins() is idempotent so a retry touches nothing. The desktop success toast is raised only when the agent half landed or a copy was actually materialised, so a refused install shows the agent error alone.
  • apps/desktop/src/app/settings/plugin-install-modal.test.tsx: two invariant tests, see below.

Validation

A/B against origin/main by swapping plugin-install-modal.tsx (cp swap, same test file):

Test main fix
never clones the desktop half standalone when the agent install is refused ✗ (installDesktopPlugin called) ✓
still clones the desktop half for a remote backend ✓ ✓
  • npx vitest run --project ui src/app/settings/plugin-install-modal.test.tsx: 8 passed.
  • npm run check:lint (tsc x3 + eslint): 0 errors (208 pre-existing warnings, none in the touched files).
  • npm --prefix apps/desktop run build: renderer builds, assert-dist-built passes.

Live repro

Hermes Desktop built from 836b5f825 with a local backend on the same HERMES_HOME, fixture https://github.com/ocuclaw/hermes-hybrid-dup-repro (git name hermes-hybrid-dup-repro, manifest name hybrid-dup-repro), both boxes ticked, Install, then Install again without Force reinstall.

Step main 836b5f8 fix
Install #1 toast Desktop plugin hybrid-dup-repro installed; ~/.hermes/desktop-plugins/ = hybrid-dup-repro/ with .hermes-package.json → ~/.hermes/plugins/hybrid-dup-repro/desktop same
Install #2, Force off dialog: Plugin 'hybrid-dup-repro' already exists. Use force reinstall or run 'hermes plugins update hybrid-dup-repro'.; ~/.hermes/desktop-plugins/ = hermes-hybrid-dup-repro/ (bare clone, no marker) and hybrid-dup-repro/ same dialog error; ~/.hermes/desktop-plugins/ = hybrid-dup-repro/ only
Capabilities → Plugins second row hermes-hybrid-dup-repro [Desktop] [on disk] [failed]: duplicate id "hybrid-dup-repro", already loaded from …/desktop-plugins/hybrid-dup-repro/plugin.js one row hybrid-dup-repro v0.1.0 Agent + Desktop git

~/.hermes/plugins/ holds one hybrid-dup-repro/ throughout in both runs.

main: retry adds hermes-hybrid-dup-repro with a duplicate id error row

fix: same retry, one row

How to Test

  1. Local backend, a hybrid repo whose plugin.yaml name: differs from the repo name (e.g. https://github.com/ocuclaw/hermes-hybrid-dup-repro: repo hermes-hybrid-dup-repro, name hybrid-dup-repro). Settings → Plugins → Install from Git, both boxes ticked, Install.
  2. ls ~/.hermes/desktop-plugins/: one folder, hybrid-dup-repro/, with .hermes-package.json.
  3. Install the same URL again without Force reinstall. Before: a second folder hermes-hybrid-dup-repro/ appears and Capabilities → Plugins shows a duplicate id error row. After: still one folder, the dialog shows only the agent's "already exists" error.

Not changed (out of scope, deliberately)

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass: N/A, renderer-only change (vitest above)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Ubuntu 24.04, Node 22

Documentation & Housekeeping

  • I've updated relevant documentation: N/A, no contract change
  • I've updated cli-config.yaml.example if I added/changed config keys: N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows: N/A
  • I've considered cross-platform impact (Windows, macOS): no path handling added, the renderer calls the existing IPC

… on a local backend

The install dialog gated the package path on the agent install succeeding in
the same click (`agentInstalled && desktopHalfFromPackage`). A package already
on disk answers "Plugin '<name>' already exists. Use force reinstall" without
Force, so the retry fell through to installDesktopPlugin and cloned
desktop-plugins/<git-name>/ beside the package copy the app had already
materialised as desktop-plugins/<manifest-name>/. Two folders, one plugin id:
the on-disk duplicate from NousResearch#100412 (since e1a6679 an error row instead of
a competing live instance, but still created by the install flow).

The package path now applies whenever the repo is a unified package, the
backend is local and the Agent box is ticked, whatever the agent install
returned. reconcileDesktopPlugins() is idempotent, so a retry touches
nothing; the desktop success toast is raised only when the agent half landed
or a copy was actually materialised, so a refused install shows the agent
error alone. A failed fresh install no longer leaves a standalone desktop
clone behind for the next successful install to duplicate.

Remote backends keep the separate clone (their plugins/ folder is not
readable from this machine); the Desktop-UI-only tick is unchanged.

Fixes NousResearch#100412 (install-time half; runtime half landed in NousResearch#118902).

Tests: two invariants in plugin-install-modal.test.tsx, the first red on
main (installDesktopPlugin called on the refused retry), the second guarding
the remote-backend clone.
@ocuclaw
ocuclaw force-pushed the fix/desktop-hybrid-install-skip-dup branch from 6120f49 to 9fbd3a9 Compare September 22, 2026 09:25
@ocuclaw

ocuclaw commented Sep 22, 2026

Copy link
Copy Markdown
Author

Re-cut on main @ 836b5f8, one commit, force-pushed to the same branch; the 09-02 diff (6120f4910) is gone from the PR but still reachable. #107314 landed the happy path and removed the door that version probed, so this is now the remaining install-time case only: the no-Force retry that clones desktop-plugins/<git-name>/ beside the package copy. Body has the live before/after on a Desktop built from that sha, the A/B table and the repro steps. CI will need a maintainer approval to run on this fork.

@ocuclaw

ocuclaw commented Sep 25, 2026

Copy link
Copy Markdown
Author

Landed via #121475 (035171c7c), cherry-picked with authorship kept. Thanks @OutThisLife.

@ocuclaw ocuclaw closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

2 participants