NousResearch/hermes-agent · error

Unknown uninstall mode: ${mode}

Error message

Unknown uninstall mode: ${mode}

What it means

ValueError from _validate in cron/notepad.py: the note key is empty. Keys are the notepad's index (per job_id), so an empty key is meaningless and rejected before touching SQLite. Enforced uniformly for set_note and any API path that goes through _validate; longer keys (> MAX_KEY_CHARS) and oversized values raise their own errors.

Source

Thrown at apps/desktop/electron/desktop-uninstall.ts:42

 * the work to a detached child that waits for this app's PID to exit, runs the
 * Python uninstall, then removes the app bundle — then the app quits. Same
 * shape as the self-update swap-and-relaunch flow already in main.ts.
 */

import path from 'node:path'

const UNINSTALL_MODES = ['gui', 'lite', 'full']

/**
 * Map an uninstall mode to the `python -m hermes_cli.uninstall` argv (after the
 * python executable). Uses the dedicated lightweight module entrypoint (not
 * `hermes_cli.main`) so it can run under a system Python OUTSIDE the venv that
 * lite/full delete — see the Finding-3 note in buildWindowsCleanupScript.
 * Throws on an unknown mode so a typo can't silently become a full wipe.
 */
function uninstallArgsForMode(mode) {
  if (!UNINSTALL_MODES.includes(mode)) {
    throw new Error(`Unknown uninstall mode: ${mode}`)
  }

  return ['-m', 'hermes_cli.uninstall', '--mode', mode]
}

/** True when `mode` removes the agent (lite/full), false for gui-only. */
function modeRemovesAgent(mode) {
  return mode === 'lite' || mode === 'full'
}

/** True when `mode` removes user data (full only). */
function modeRemovesUserData(mode) {
  return mode === 'full'
}

/**
 * Resolve the on-disk app bundle/dir to remove for the running desktop app,
 * given the path to the running executable (`process.execPath`) and platform.

View on GitHub (pinned to c896c09c42)

Solutions

  1. Always pass a descriptive non-empty key: 'last-status', 'run-count', etc.
  2. Filter or default empty keys before writing: key = key or 'default'.
  3. Keep keys under MAX_KEY_CHARS and values under MAX_VALUE_BYTES to avoid the sibling size errors.

Example fix

# before
set_note(job_id, "", "ran ok")

# after
set_note(job_id, "last-status", "ran ok")
Defensive patterns

Strategy: validation

Validate before calling

def valid_note_key(key: str) -> bool:
    return bool(key)  # add len(key) <= MAX_KEY_CHARS for full pre-validation

Type guard

def is_note_key(v) -> bool:
    """True when v is a non-empty, bounded notepad key."""
    from cron.notepad import MAX_KEY_CHARS
    return isinstance(v, str) and 0 < len(v) <= MAX_KEY_CHARS

Try / catch

try:
    set_note(job_id, key, value)
except ValueError as e:
    if "key must be non-empty" in str(e):
        set_note(job_id, key or "misc", value)  # only if a default key is meaningful; else raise
        raise

Prevention

When it happens

Trigger: set_note(job_id, '', 'value'); keys built from a variable that is empty for some loop iterations; agent-generated note writes where the model emitted a value but no key.

Common situations: Programmatic loops deriving keys from data where some records lack the key field; tool-call parameter marshalling dropping empty-string defaults; refactors that swapped the key/value argument order leaving key empty.

Related errors


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