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