NousResearch/hermes-agent

Desktop crashes when second instance opens after window closed

Cluster 50491 · apps/desktop. Data as of

Nothing open on GitHub. Fix merged in #64800 on 16 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #64800. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #64800. That is separate from the fix event.
Still open
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.

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.

  • #64800 fix(desktop): restore closed main window on second launch

    merged to main

    files: apps/desktop/electron/main-window-lifecycle.test.ts, apps/desktop/electron/main-window-lifecycle.ts, apps/desktop/electron/main.ts

    Show the diff
    • apps/desktop/electron/main-window-lifecycle.test.ts
      @@ -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
      @@ -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
      @@ -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 {
      @@ -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'))
       
      @@ -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
      +    })
         })
       }
       

1 PR closed without merging

From the close comments: other 1

  • #50515 fix(desktop): guard second-instance focus against destroyed mainWindow (#50491) · other
Closed issues · 1
  • Closed#50491[Bug]: Hermes Desktop crashes with "Object has been destroyed" when second-instance fires after mainWindow is closed
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: 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
#64800
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
16 Jul 2026

For your agent

The same facts, typed.