# NousResearch/hermes-agent · Desktop crashes when second instance opens after window closed

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #64800. Verify it addresses your case.
- **Recommended:** read PR #64800 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-64800`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #50491: \[Bug\]: Hermes Desktop crashes with "Object has been destroyed" when second-instance fires after mainWindow is closed

## Prior & competing PRs

- **#50515: fix(desktop): guard second-instance focus against destroyed mainWindow (#50491)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#64800: fix(desktop): restore closed main window on second launch**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/electron/main-window-lifecycle.test.ts`, `apps/desktop/electron/main-window-lifecycle.ts`, `apps/desktop/electron/main.ts`
  - fix hunks:
  - `apps/desktop/electron/main-window-lifecycle.test.ts`

    ```diff
    @@ -0,0 +1,69 @@
    +import assert from 'node:assert/strict'
    +import { test } from 'vitest'
    +
    +import { ensureMainWindow } from './main-window-lifecycle'
    +
    +test('recreates a destroyed primary window without focusing it', () => {
    +  const destroyedWindow = {
    +    isDestroyed: () => true
    +  }
    +  let createCalls = 0
    +  let focusCalls = 0
    +
    +  ensureMainWindow(destroyedWindow, {
    +    isReady: true,
    +    createWindow: () => {
    +      createCalls += 1
    +    },
    +    focusWindow: () => {
    +      focusCalls += 1
    +    }
    +  })
    +
    +  assert.equal(createCalls, 1)
    +  assert.equal(focusCalls, 0)
    +})
    +
    +test('waits for app readiness before recreating a primary window', () => {
    +  let createCalls = 0
    +
    +  ensureMainWindow(null, {
    +    isReady: false,
    +    createWindow: () => {
    +      createCalls += 1
    +    },
    +    focusWindow: () => assert.fail('missing window must not be focused')
    +  })
    +
    +  assert.equal(createCalls, 0)
    +})
    +
    +test('focuses a live primary window for a normal second launch', () => {
    +  const liveWindow = {
    +    isDestroyed: () => false
    +  }
    +  let focusedWindow = null
    +
    +  ensureMainWindow(liveWindow, {
    +    isReady: true,
    +    createWindow: () => assert.fail('live window must not be replaced'),
    +    focusWindow: window => {
    +      focusedWindow = window
    +    }
    +  })
    +
    +  assert.equal(focusedWindow, liveWindow)
    +})
    +
    +test('leaves live-window focus to deep-link delivery', () => {
    +  const liveWindow = {
    +    isDestroyed: () => false
    +  }
    +
    +  ensureMainWindow(liveWindow, {
    +    isReady: true,
    +    createWindow: () => assert.fail('live window must not be replaced'),
    +    focusWindow: () => assert.fail('deep-link delivery owns focus'),
    +    focusExisting: false
    +  })
    +})
    ```
  - `apps/desktop/electron/main-window-lifecycle.ts`

    ```diff
    @@ -0,0 +1,28 @@
    +type MainWindowLike = {
    +  isDestroyed: () => boolean
    +}
    +
    +type EnsureMainWindowOptions<T extends MainWindowLike> = {
    +  isReady: boolean
    +  createWindow: () => unknown
    +  focusWindow: (window: T) => unknown
    +  focusExisting?: boolean
    +}
    +
    +export function ensureMainWindow<T extends MainWindowLike>(
    +  window: T | null | undefined,
    +  { isReady, createWindow, focusWindow, focusExisting = true }: EnsureMainWindowOptions<T>
    +) {
    +  if (!window || window.isDestroyed()) {
    +    // a closed electron window stays truthy, so replace it before invoking native methods.
    +    if (isReady) {
    +      createWindow()
    +    }
    +
    +    return
    +  }
    +
    +  if (focusExisting) {
    +    focusWindow(window)
    +  }
    +}
    ```
  - `apps/desktop/electron/main.ts`

    ```diff
    @@ -95,6 +95,7 @@ import {
       TEXT_PREVIEW_SOURCE_MAX_BYTES
     } from './hardening'
     import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window'
    +import { ensureMainWindow } from './main-window-lifecycle'
     import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request'
     import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'
     import {
    ```

    ```diff
    @@ -7234,10 +7235,17 @@ function createWindow() {
       mainWindow.on('unmaximize', schedulePersistWindowState)
       mainWindow.on('close', () => schedulePersistWindowState.flush())
     
    -  // The overlay rides the main window — closing the app's primary window must
    -  // tear it down too (otherwise it strands as an orphan that blocks
    -  // window-all-closed from quitting on Windows/Linux).
    -  mainWindow.on('closed', () => closePetOverlay())
    +  // the closed wrapper remains truthy, so clear only the window this callback owns.
    +  const createdMainWindow = mainWindow
    +  mainWindow.on('closed', () => {
    +    closePetOverlay()
    +
    +    if (mainWindow === createdMainWindow) {
    +      mainWindow = null
    +      // the replacement renderer must register before queued links can be delivered.
    +      _rendererReadyForDeepLink = false
    +    }
    +  })
     
       wireCommonWindowHandlers(mainWindow, zoomWiringForWindowKind('chat'))
     
    ```

    ```diff
    @@ -8995,13 +9003,15 @@ if (!_gotSingleInstanceLock) {
     
         if (url) {
           handleDeepLink(url)
    -    } else if (mainWindow) {
    -      if (mainWindow.isMinimized()) {
    -        mainWindow.restore()
    -      }
    -
    -      mainWindow.focus()
         }
    +
    +    ensureMainWindow(mainWindow, {
    +      isReady: app.isReady(),
    +      createWindow,
    +      focusWindow,
    +      // deep-link delivery focuses a live window after its renderer is ready.
    +      focusExisting: !url
    +    })
       })
     }
     
    ```

## Fix status

- Merged to main in PR #64800 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-16`.
- Subsystem: `apps/desktop`
