Windows desktop install, update, and packaging fixes

NousResearch/hermes-agent · cluster 38016 · apps/desktop

Data as of

Nothing open on GitHub. Fix merged in #40588. Check it covers your case.

The short answer

0open issues
0open PRs
7items grouped
Known?
Yes. 7 related items grouped here, 3 issues already closed.
Fix event
A fix was merged to main in PR #40588. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #40588. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests address failures in the Hermes desktop app on Windows, covering TLS certificate errors during install, Electron binary download problems, self-update rebuild failures, and packaging issues with win-unpacked. Most members relate to Windows-specific installation or update reliability.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #40588 fix(install): detect TLS cert-trust failures during npm install on Windows

    merged to main

    files: agent/conversation_loop.py, scripts/install.ps1, tests/run_agent/test_413_compression.py, tests/run_agent/test_run_agent.py

    Show the diff
    • agent/conversation_loop.py
      @@ -2720,6 +2720,61 @@ def _perform_api_call(next_api_kwargs):
                       # compress history and retry, not abort immediately.
                       status_code = getattr(api_error, "status_code", None)
       
      +                # ── Respect disabled auto-compaction on overflow ──────
      +                # Ported from anomalyco/opencode#30749.  When the user has
      +                # turned auto-compaction off (``compression.enabled: false``),
      +                # NO automatic compaction trigger may fire — including the
      +                # provider/request-size overflow recovery paths below
      +                # (long-context-tier 429, 413 payload-too-large, and
      +                # context-overflow).  Without this guard the proactive
      +                # threshold path correctly honours the setting (see the
      +                # preflight check and the post-response ``should_compress``
      +                # gate) but a provider overflow error would still silently
      +                # compress + rotate the session, bypassing the user's
      +                # explicit choice.  Surface a terminal error instead so the
      +                # user can compact manually (``/compress``), start fresh
      +                # (``/new``), switch to a larger-context model, or reduce
      +                # attachments.  Forced compaction via ``/compress``
      +                # (``force=True``) is unaffected — it never reaches this loop.
      +                _overflow_reasons = {
      +                    FailoverReason.long_context_tier,
      +                    FailoverReason.payload_too_large,
      +                    FailoverReason.context_overflow,
      +                }
      +                if (
      +                    classified.reason in _overflow_reasons
      +                    and not getattr(agent, "compression_enabled", True)
      +                ):
      +                    agent._flush_status_buffer()
      +                    agent._vprint(
      +                        f"{agent.log_prefix}❌ Context overflow, but auto-compaction is disabled "
      +                        f"(compression.enabled: false).",
      +                        force=True,
      +                    )
      +                    agent._vprint(
      +                        f"{agent.log_prefix}   💡 Run /compress to compact manually, /new to start fresh, "
      +                        f"switch to a larger-context model, or reduce attachments.",
      +                        force=True,
      +                    )
      +                    logger.error(
      +                        f"{agent.log_prefix}Context overflow ({classified.reason.value}) with "
      +                        f"auto-compaction disabled — not compressing."
      +                    )
      +                    agent._persist_session(messages, conversation_history)
      +                    return {
      +                        "messages": messages,
      +                        "completed": False,
      +                        "api_calls": api_call_count,
      +                        "error": (
      +                            "Context overflow and auto-compaction is disabled "
      +                            "(compression.enabled: false). Run /compress to compact manually, "
      +                            "/new to start fresh, or switch to a larger-context model."
      +                        ),
      +                        "partial": True,
      +                        "failed": True,
      +                        "compaction_disabled": True,
      +                    }
      +
                       # ── Anthropic Sonnet long-context tier gate ───────────
                       # Anthropic returns HTTP 429 "Extra usage is required for
                       # long context requests" when a Claude Max (or similar)
    • scripts/install.ps1
      @@ -185,6 +185,35 @@ function Write-Err {
           Write-Host "[X] $Message" -ForegroundColor Red
       }
       
      +# Inspect npm output for a TLS-trust failure and, if found, print actionable
      +# remediation. npm/Node surface corporate MITM proxies and missing root CAs as
      +# "unable to get local issuer certificate" / "self-signed certificate in
      +# certificate chain" / UNABLE_TO_GET_ISSUER_CERT_LOCALLY -- most commonly while
      +# Electron's install.js postinstall downloads the Electron binary. The reporter
      +# usually misreads this as an admin-rights or generic install failure (see
      +# issue #38016), so detect it once here and route every npm stage through this
      +# hint. Returns $true when a cert error was detected (caller may adjust its own
      +# messaging), $false otherwise.
      +function Show-NpmCertHint {
      +    param([string]$NpmOutput)
      +    if (-not $NpmOutput) { return $false }
      +    $isCertError = $NpmOutput -match "unable to get local issuer certificate" `
      +        -or $NpmOutput -match "self.signed certificate" `
      +        -or $NpmOutput -match "UNABLE_TO_GET_ISSUER_CERT_LOCALLY" `
      +        -or $NpmOutput -match "SELF_SIGNED_CERT_IN_CHAIN" `
      +        -or $NpmOutput -match "CERT_HAS_EXPIRED"
      +    if (-not $isCertError) { return $false }
      +    Write-Warn "This looks like a TLS certificate-trust failure, not a permissions problem."
      +    Write-Info "  A corporate proxy or antivirus is likely intercepting HTTPS and presenting a"
      +    Write-Info "  certificate Node.js doesn't trust. To fix, point Node at your org's root CA:"
      +    Write-Info "    1. Get the corporate root CA as a .pem/.crt from your IT team."
      +    Write-Info "    2. setx NODE_EXTRA_CA_CERTS `"C:\path\to\corp-ca.pem`""
      +    Write-Info "    3. Open a NEW terminal (so the env var takes effect) and re-run the installer."
      +    Write-Info "  Quick (less secure) alternative -- disable TLS verification just for the install:"
      +    Write-Info "    npm config set strict-ssl false   (re-enable afterwards: npm config set strict-ssl true)"
      +    return $true
      +}
      +
       # --- Ensure-mode helpers ---
       
       function Resolve-NpmCmd {
      @@ -252,6 +281,7 @@ function Install-AgentBrowser {
               $npmDetail = Get-Content $npmLog -Raw -ErrorAction SilentlyContinue
               Remove-Item $npmLog -Force -ErrorAction SilentlyContinue
               Write-Err "npm install -g failed (exit $npmExit): $npmDetail"
      +        Show-NpmCertHint $npmDetail | Out-Null
               throw "npm install failed"
           }
           Remove-Item $npmLog -Force -ErrorAction SilentlyContinue
      @@ -1809,6 +1839,7 @@ function Install-NodeDeps {
                               Write-Host "    $line" -ForegroundColor DarkGray
                           }
                           Write-Info "  Full log: $logPath"
      +                    Show-NpmCertHint $errText | Out-Null
                       }
                   }
                   Write-Info "Run manually later: cd `"$installDir`"; npm install"
      @@ -2014,15 +2045,22 @@ function Install-Desktop {
               # tsc/typescript unresolved so `npm run pack`'s `tsc -b` dies with
               # no obvious cause. Fall back to `npm install` only if `npm ci`
               # fails (lockfile out of sync / very old npm without ci).
      -        & $npmExe ci 2>&1 | ForEach-Object { "$_" }
      +        #
      +        # Tee the merged output into $npmOut while still emitting every line
      +        # live. We don't need a side log file (the bootstrap streaming sink
      +        # is the artifact), but on failure we scan $npmOut for the TLS-trust
      +        # signature so corporate-proxy users get the NODE_EXTRA_CA_CERTS hint
      +        # instead of an opaque "exit 1" (issue #38016).
      +        & $npmExe ci 2>&1 | ForEach-Object { "$_" } | Tee-Object -Variable npmOut
               $code = $LASTEXITCODE
               if ($code -ne 0) {
                   Write-Info "  npm ci failed (exit $code) -- retrying with npm install..."
      -            & $npmExe install 2>&1 | ForEach-Object { "$_" }
      +            & $npmExe install 2>&1 | ForEach-Object { "$_" } | Tee-Object -Variable npmOut
                   $code = $LASTEXITCODE
               }
               $ErrorActionPreference = $prevEAP
               if ($code -ne 0) {
      +            Show-NpmCertHint ($npmOut -join "`n") | Out-Null
                   throw "desktop workspace npm install failed (exit $code) -- see lines above for cause"
               }
               Write-Success "Desktop workspace dependencies installed"
    • tests/run_agent/test_413_compression.py
      @@ -94,7 +94,11 @@ def agent():
               a._cached_system_prompt = "You are helpful."
               a._use_prompt_caching = False
               a.tool_delay = 0
      -        a.compression_enabled = False
      +        # Default matches production (`compression.enabled` defaults to True).
      +        # Overflow-recovery tests below verify that 413 / context-overflow
      +        # errors DO trigger compression; the disabled-path behavior is
      +        # covered explicitly by TestOverflowWithCompactionDisabled.
      +        a.compression_enabled = True
               a.save_trajectories = False
               return a
       
      @@ -415,6 +419,13 @@ class TestPreflightCompression:
       
           def test_compress_context_emits_lifecycle_status_before_work(self, agent):
               """Direct context compression should tell gateway users why the turn paused."""
      +        # This test calls _compress_context directly and asserts the FIRST
      +        # status event is the lifecycle "Compacting context" message. With
      +        # compaction enabled the lazy feasibility probe would emit an
      +        # aux-provider warning first (no aux key in the hermetic test env),
      +        # displacing events[0]. The flag value is irrelevant to what this
      +        # test asserts, so disable it to suppress the probe.
      +        agent.compression_enabled = False
               events = []
               agent.status_callback = lambda ev, msg: events.append((ev, msg))
       
      @@ -802,3 +813,95 @@ def test_anthropic_prompt_too_long_safety_net(self, agent):
       
               mock_compress.assert_called_once()
               assert result["completed"] is True
      +
      +
      +# ---------------------------------------------------------------------------
      +# Disabled auto-compaction on overflow (port of anomalyco/opencode#30749)
      +# ---------------------------------------------------------------------------
      +
      +class TestOverflowWithCompactionDisabled:
      +    """When ``compression.enabled`` is False, NO automatic compaction may
      +    fire — including the provider/request-size overflow recovery paths.
      +
      +    Ported from anomalyco/opencode#30749: the proactive token-threshold
      +    path already honoured the setting, but provider overflow errors
      +    (413 payload-too-large, context-overflow, long-context-tier 429) still
      +    silently compressed + rotated the session. The fix surfaces a terminal
      +    error so the user can compact manually, start fresh, or switch models.
      +    """
      +
      +    @staticmethod
      +    def _prefill():
      +        return [
      +            {"role": "user", "content": "previous question"},
      +            {"role": "assistant", "content": "previous answer"},
      +        ]
      +
      +    def test_413_does_not_compress_when_disabled(self, agent):
      +        """413 must NOT call _compress_context when compaction is disabled."""
      +        agent.compression_enabled = False
      +        err_413 = _make_413_error()
      +        # If the guard fails, a second (success) response would be consumed.
      +        agent.client.chat.completions.create.side_effect = [err_413, _mock_response()]
      +
      +        with (
      +            patch.object(agent, "_compress_context") as mock_compress,
      +            patch.object(agent, "_persist_session") as mock_persist,
      +            patch.object(agent, "_save_trajectory"),
      +            patch.object(agent, "_cleanup_task_resources"),
      +        ):
      +            result = agent.run_conversation("hello", conversation_history=self._prefill())
      +
      +        mock_compress.assert_not_called()
      +        mock_persist.assert_called()
      +        assert result.get("failed") is True
      +        assert result.get("compaction_disabled") is True
      +        assert "auto-compaction is disabled" in result["error"]
      +
      +    def test_context_overflow_does_not_compress_when_disabled(self, agent):
      +        """400 'prompt is too long' must NOT compress when compaction disabled."""
      +        agent.compression_enabled = False
      +        err_400 = Exception(
      +            "Error code: 400 - {'type': 'error', 'error': {'type': "
      +            "'invalid_request_error', 'message': 'prompt is too long: "
      +            "233153 tokens > 200000 maximum'}}"
      +        )
      +        err_400.status_code = 400
      +        agent.client.chat.completions.create.side_effect = [err_400, _mock_response()]
      +
      +        with (
      +            patch.object(agent, "_compress_context") as mock_compress,
      +            patch.object(agent, "_persist_session"),
      +            patch.object(agent, "_save_trajectory"),
      +            patch.object(agent, "_cleanup_task_resources"),
      +        ):
      +            result = agent.run_conversation("hello", conversation_history=self._prefill())
      +
      +        mock_compress.assert_not_called()
      +        assert result.get("compaction_disabled") is True
      +
      +    def test_413_still_compresses_when_enabled(self, agent):
      +        """Control: with compaction enabled, 413 still triggers compression.
      +
      +        Guards against the disabled-path guard accidentally swallowing the
      +        enabled path.
      +        """
      +        agent.compression_enabled = True
      +        err_413 = _make_413_error()
      +        ok_resp = _mock_response(content="Recovered", finish_reason="stop")
      +        agent.client.chat.completions.create.side_effect = [err_413, ok_resp]
      +
      +        with (
      +            patch.object(agent, "_compress_context") as mock_compress,
      +            patch.object(agent, "_persist_session"),
      +            patch.object(agent, "_save_trajectory"),
      +            patch.object(agent, "_cleanup_task_resources"),
      +        ):
      +            mock_compress.return_value = (
      +                [{"role": "user", "content": "hello"}], "compressed",
      +            )
      +            result = agent.run_conversation("hello", conversation_history=self._prefill())
      +
      +        mock_compress.assert_called_once()
      +        assert result["completed"] is True
      +        assert result.get("compaction_disabled") is not True
    • tests/run_agent/test_run_agent.py
      @@ -3903,6 +3903,7 @@ def test_context_compression_triggered(self, agent):
           def test_glm_prompt_exceeds_max_length_triggers_compression(self, agent):
               """GLM/Z.AI uses 'Prompt exceeds max length' for context overflow."""
               self._setup_agent(agent)
      +        agent.compression_enabled = True  # this test verifies overflow→compression fires
               err_400 = Exception(
                   "Error code: 400 - {'error': {'code': '1261', 'message': 'Prompt exceeds max length'}}"
               )
      @@ -3937,6 +3938,7 @@ def test_minimax_delta_overflow_keeps_known_context_length(self, agent):
               to the generic 128K fallback tier.
               """
               self._setup_agent(agent)
      +        agent.compression_enabled = True  # this test verifies overflow→compression fires
               agent.provider = "minimax"
               agent.model = "MiniMax-M2.7-highspeed"
               agent.base_url = "https://api.minimax.io/anthropic"
      @@ -3982,6 +3984,7 @@ def test_non_minimax_overflow_without_provider_limit_keeps_context(self, agent):
               rely on compression — see #33669 / PR #33826.
               """
               self._setup_agent(agent)
      +        agent.compression_enabled = True  # this test verifies overflow→compression fires
               agent.provider = "openrouter"
               agent.model = "some/unknown-model"
               agent.base_url = "https://openrouter.ai/api/v1"
  • #101336 fix(update): Desktop rebuild stages and swaps so a failed pack never removes the working app (#86443, salvage #44234/#78431)

    merged to main

    files: .gitignore, contributors/emails/[redacted:email], hermes_cli/main.py, tests/hermes_cli/test_desktop_exe_integrity.py, tests/hermes_cli/test_gui_command.py, website/docs/getting-started/updating.md

2 PRs closed without merging

From the close comments: done elsewhere 1 · superseded by another PR 1

  • #44234 fix(desktop): preserve the packaged app when an Electron rebuild fails · done elsewhere → b1193b2, #101336
  • #44238 fix(update): backup desktop executable before rebuild to prevent loss on build failure · superseded by another PR → #44234
Closed issues · 3
  • Closed#38016[Setup]: Hermes Desktop failed on Windows 11 without admin right
  • Closed#44225hermes update destroys Hermes Desktop executable on failed Electron rebuild (desktop shortcut becomes dead link)
  • Closed#46939[Bug]: 15/6 hermes desktop更新失败
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.22: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
apps/desktop
Merged PRs
#40588, #101336
Closed issues
3
Attempts
2 PRs closed without merging
Fix link checked
2 Sep 2026

For your agent

The same facts, typed.