NousResearch/hermes-agent · warning

An update is already in progress.

Error message

An update is already in progress.

What it means

A re-entrancy guard in applyUpdates: the module-level updateInFlight flag is already true, meaning an update handoff is in progress. The desktop never updates itself; it spawns the Hermes-Setup binary with --update and quits, and this flag prevents a second concurrent trigger (double-click, queued IPC) from starting a second updater.

Source

Thrown at apps/desktop/electron/main.ts:2894

    `[${tag}] venv shim still locked after 15s; aborting hand-off (something outside this app holds the venv)`
  )

  return { unlocked: false }
}

// applyUpdates — hand off to the installer's --update flow, then exit.
//
// The desktop is a pure consumer: it does NOT git pull / pip install / rebuild
// itself (the old open-coded git dance lived here and drifted from
// `hermes update`). Instead we spawn the staged Hermes-Setup binary with
// --update and quit, so it can run `hermes update` (which refuses while we
// hold the venv shim) and rebuild the desktop with our exe already gone.
//
// Detection (checkUpdates / commit changelog / "N behind") stays in the UI;
// only this apply action changed.
async function applyUpdates(opts = {}) {
  if (updateInFlight) {
    throw new Error('An update is already in progress.')
  }

  updateInFlight = true

  try {
    const updater = resolveUpdaterBinary()

    if (!updater && !IS_WINDOWS) {
      // macOS/Linux: hand off to the repo-owned posix script — same shape as
      // Windows (quit → detached orchestrator → `hermes update` → relaunch),
      // minus the venv-lock gauntlet POSIX doesn't need. The old in-app
      // updater (applyUpdatesPosixInApp) is gone with everything it dragged
      // in: the HERMES_DESKTOP_CHILD_PID reaper-exclusion dance (#37532),
      // the in-window rebuild retry, and the relaunch-outcome matrix — the
      // script owns swap/relaunch, and the app is DEAD during the update so
      // there is nothing to reap around. Checkouts that predate the script
      // get the manual `hermes update` card once; their next update pulls it.
      return await applyUpdatesPosixHandoff(opts)

View on GitHub (pinned to c896c09c42)

Solutions

  1. Wait for the in-flight update to finish — the app will quit and relaunch via the installer flow.
  2. If the app seems stuck (no quit after a long wait), restart the desktop app and trigger the update once.
  3. In renderer code, disable the update button as soon as the first applyUpdates call begins.

Example fix

// before
updateButton.onclick = () => void ipc.invoke('updates:apply')

// after — disable on first click to avoid the guard firing
updateButton.onclick = () => {
  updateButton.disabled = true
  ipc.invoke('updates:apply').finally(() => (updateButton.disabled = false))
}
Defensive patterns

Strategy: type-guard

Type guard

// Guard at the renderer: only one apply at a time
let applyingUpdate = false
async function safeApplyUpdates() {
  if (applyingUpdate) return // update already in progress
  applyingUpdate = true
  try { await applyUpdates() } finally { applyingUpdate = false }
}

Try / catch

try {
  await applyUpdates()
} catch (e) {
  if (/already in progress/.test(e.message)) return // benign duplicate trigger
  throw e
}

Prevention

When it happens

Trigger: Invoking the apply-update IPC twice in quick succession (double-click on the update button, or the renderer firing the action again before the app quits); an update handoff that started but the quit is slow.

Common situations: Impatient double-clicks on 'Restart & Update'; race between an auto-triggered update and a manual one.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/6c26949396d1fc4b. Report an issue: GitHub.