{"record":{"id":"6c26949396d1fc4b","repo":"NousResearch/hermes-agent","slug":"an-update-is-already-in-progress","errorCode":null,"errorMessage":"An update is already in progress.","messagePattern":"An update is already in progress\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/desktop/electron/main.ts","lineNumber":2894,"sourceCode":"    `[${tag}] venv shim still locked after 15s; aborting hand-off (something outside this app holds the venv)`\n  )\n\n  return { unlocked: false }\n}\n\n// applyUpdates — hand off to the installer's --update flow, then exit.\n//\n// The desktop is a pure consumer: it does NOT git pull / pip install / rebuild\n// itself (the old open-coded git dance lived here and drifted from\n// `hermes update`). Instead we spawn the staged Hermes-Setup binary with\n// --update and quit, so it can run `hermes update` (which refuses while we\n// hold the venv shim) and rebuild the desktop with our exe already gone.\n//\n// Detection (checkUpdates / commit changelog / \"N behind\") stays in the UI;\n// only this apply action changed.\nasync function applyUpdates(opts = {}) {\n  if (updateInFlight) {\n    throw new Error('An update is already in progress.')\n  }\n\n  updateInFlight = true\n\n  try {\n    const updater = resolveUpdaterBinary()\n\n    if (!updater && !IS_WINDOWS) {\n      // macOS/Linux: hand off to the repo-owned posix script — same shape as\n      // Windows (quit → detached orchestrator → `hermes update` → relaunch),\n      // minus the venv-lock gauntlet POSIX doesn't need. The old in-app\n      // updater (applyUpdatesPosixInApp) is gone with everything it dragged\n      // in: the HERMES_DESKTOP_CHILD_PID reaper-exclusion dance (#37532),\n      // the in-window rebuild retry, and the relaunch-outcome matrix — the\n      // script owns swap/relaunch, and the app is DEAD during the update so\n      // there is nothing to reap around. Checkouts that predate the script\n      // get the manual `hermes update` card once; their next update pulls it.\n      return await applyUpdatesPosixHandoff(opts)","sourceCodeStart":2876,"sourceCodeEnd":2912,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L2876-L2912","documentation":"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.","triggerScenarios":"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.","commonSituations":"Impatient double-clicks on 'Restart & Update'; race between an auto-triggered update and a manual one.","solutions":["Wait for the in-flight update to finish — the app will quit and relaunch via the installer flow.","If the app seems stuck (no quit after a long wait), restart the desktop app and trigger the update once.","In renderer code, disable the update button as soon as the first applyUpdates call begins."],"exampleFix":"// before\nupdateButton.onclick = () => void ipc.invoke('updates:apply')\n\n// after — disable on first click to avoid the guard firing\nupdateButton.onclick = () => {\n  updateButton.disabled = true\n  ipc.invoke('updates:apply').finally(() => (updateButton.disabled = false))\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Guard at the renderer: only one apply at a time\nlet applyingUpdate = false\nasync function safeApplyUpdates() {\n  if (applyingUpdate) return // update already in progress\n  applyingUpdate = true\n  try { await applyUpdates() } finally { applyingUpdate = false }\n}","tryCatchPattern":"try {\n  await applyUpdates()\n} catch (e) {\n  if (/already in progress/.test(e.message)) return // benign duplicate trigger\n  throw e\n}","preventionTips":["Disable the update button on first click","Treat 'already in progress' as a no-op, not an error","Debounce rapid IPC triggers in the renderer"],"tags":["desktop","update","concurrency","guard","electron"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}