{"repo":"NousResearch/hermes-agent","public_id":"50491","display_label":"Desktop crashes when second instance opens after window closed","summary":"A bug where Hermes Desktop crashes with 'Object has been destroyed' when a second instance is launched after the main window has been closed. The members cover the bug report and related fixes guarding or restoring the main window in the second-instance handler.","subsystem":"apps/desktop","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":64800,"merged_at":"2026-07-16T19:33:38+00:00"},"resolution":{"state":"resolved","by":"pr-64800"},"freshness":{"state":"verified","last_verified_at":"2026-07-16T19:33:38+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":50491,"pr_number":64800,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-16T19:33:38+00:00"}],"members":[{"kind":"issue","number":50491,"title":"[Bug]: Hermes Desktop crashes with \"Object has been destroyed\" when second-instance fires after mainWindow is closed","url":null,"state":"closed"},{"kind":"pr","number":50515,"title":"fix(desktop): guard second-instance focus against destroyed mainWindow (#50491)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":64800,"title":"fix(desktop): restore closed main window on second launch","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main-window-lifecycle.test.ts","apps/desktop/electron/main-window-lifecycle.ts","apps/desktop/electron/main.ts"],"fix_hunks":[{"file":"apps/desktop/electron/main-window-lifecycle.test.ts","hunks":["@@ -0,0 +1,69 @@\n+import assert from 'node:assert/strict'\n+import { test } from 'vitest'\n+\n+import { ensureMainWindow } from './main-window-lifecycle'\n+\n+test('recreates a destroyed primary window without focusing it', () => {\n+  const destroyedWindow = {\n+    isDestroyed: () => true\n+  }\n+  let createCalls = 0\n+  let focusCalls = 0\n+\n+  ensureMainWindow(destroyedWindow, {\n+    isReady: true,\n+    createWindow: () => {\n+      createCalls += 1\n+    },\n+    focusWindow: () => {\n+      focusCalls += 1\n+    }\n+  })\n+\n+  assert.equal(createCalls, 1)\n+  assert.equal(focusCalls, 0)\n+})\n+\n+test('waits for app readiness before recreating a primary window', () => {\n+  let createCalls = 0\n+\n+  ensureMainWindow(null, {\n+    isReady: false,\n+    createWindow: () => {\n+      createCalls += 1\n+    },\n+    focusWindow: () => assert.fail('missing window must not be focused')\n+  })\n+\n+  assert.equal(createCalls, 0)\n+})\n+\n+test('focuses a live primary window for a normal second launch', () => {\n+  const liveWindow = {\n+    isDestroyed: () => false\n+  }\n+  let focusedWindow = null\n+\n+  ensureMainWindow(liveWindow, {\n+    isReady: true,\n+    createWindow: () => assert.fail('live window must not be replaced'),\n+    focusWindow: window => {\n+      focusedWindow = window\n+    }\n+  })\n+\n+  assert.equal(focusedWindow, liveWindow)\n+})\n+\n+test('leaves live-window focus to deep-link delivery', () => {\n+  const liveWindow = {\n+    isDestroyed: () => false\n+  }\n+\n+  ensureMainWindow(liveWindow, {\n+    isReady: true,\n+    createWindow: () => assert.fail('live window must not be replaced'),\n+    focusWindow: () => assert.fail('deep-link delivery owns focus'),\n+    focusExisting: false\n+  })\n+})"]},{"file":"apps/desktop/electron/main-window-lifecycle.ts","hunks":["@@ -0,0 +1,28 @@\n+type MainWindowLike = {\n+  isDestroyed: () => boolean\n+}\n+\n+type EnsureMainWindowOptions<T extends MainWindowLike> = {\n+  isReady: boolean\n+  createWindow: () => unknown\n+  focusWindow: (window: T) => unknown\n+  focusExisting?: boolean\n+}\n+\n+export function ensureMainWindow<T extends MainWindowLike>(\n+  window: T | null | undefined,\n+  { isReady, createWindow, focusWindow, focusExisting = true }: EnsureMainWindowOptions<T>\n+) {\n+  if (!window || window.isDestroyed()) {\n+    // a closed electron window stays truthy, so replace it before invoking native methods.\n+    if (isReady) {\n+      createWindow()\n+    }\n+\n+    return\n+  }\n+\n+  if (focusExisting) {\n+    focusWindow(window)\n+  }\n+}"]},{"file":"apps/desktop/electron/main.ts","hunks":["@@ -95,6 +95,7 @@ import {\n   TEXT_PREVIEW_SOURCE_MAX_BYTES\n } from './hardening'\n import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window'\n+import { ensureMainWindow } from './main-window-lifecycle'\n import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request'\n import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'\n import {","@@ -7234,10 +7235,17 @@ function createWindow() {\n   mainWindow.on('unmaximize', schedulePersistWindowState)\n   mainWindow.on('close', () => schedulePersistWindowState.flush())\n \n-  // The overlay rides the main window — closing the app's primary window must\n-  // tear it down too (otherwise it strands as an orphan that blocks\n-  // window-all-closed from quitting on Windows/Linux).\n-  mainWindow.on('closed', () => closePetOverlay())\n+  // the closed wrapper remains truthy, so clear only the window this callback owns.\n+  const createdMainWindow = mainWindow\n+  mainWindow.on('closed', () => {\n+    closePetOverlay()\n+\n+    if (mainWindow === createdMainWindow) {\n+      mainWindow = null\n+      // the replacement renderer must register before queued links can be delivered.\n+      _rendererReadyForDeepLink = false\n+    }\n+  })\n \n   wireCommonWindowHandlers(mainWindow, zoomWiringForWindowKind('chat'))\n ","@@ -8995,13 +9003,15 @@ if (!_gotSingleInstanceLock) {\n \n     if (url) {\n       handleDeepLink(url)\n-    } else if (mainWindow) {\n-      if (mainWindow.isMinimized()) {\n-        mainWindow.restore()\n-      }\n-\n-      mainWindow.focus()\n     }\n+\n+    ensureMainWindow(mainWindow, {\n+      isReady: app.isReady(),\n+      createWindow,\n+      focusWindow,\n+      // deep-link delivery focuses a live window after its renderer is ready.\n+      focusExisting: !url\n+    })\n   })\n }\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}