NousResearch/hermes-agent · error

Desktop installation ID is owned by another user.

Error message

Desktop installation ID is owned by another user.

What it means

ValueError from resume_job (cron/jobs.py:2009): resuming a paused one-shot job whose run_at is beyond the 120s grace window — compute_next_run returns None, so if resumed the job would sit scheduled forever and never fire. The error tells you up front instead of leaving a zombie job.

Source

Thrown at apps/desktop/electron/desktop-installation.ts:96

      continue
    }

    try {
      const winner = readInstallationId(filePath)

      if (winner) {
        return winner
      }

      try {
        const stat = fs.lstatSync(filePath)

        if (!stat.isFile() && !stat.isSymbolicLink()) {
          throw new Error('Desktop installation ID path is not a regular file.')
        }

        if (!stat.isSymbolicLink() && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
          throw new Error('Desktop installation ID is owned by another user.')
        }

        fs.unlinkSync(filePath)
      } catch (error: any) {
        if (error?.code !== 'ENOENT') {
          throw error
        }
      }

      fs.writeFileSync(filePath, JSON.stringify({ installationId }), { encoding: 'utf8', flag: 'wx', mode: 0o600 })

      return installationId
    } finally {
      if (repairFd !== undefined) {
        fs.closeSync(repairFd)
      }

      try {

View on GitHub (pinned to c896c09c42)

Solutions

  1. Resume with a new future schedule in one step: update_job(id, {'enabled': True, 'state': 'scheduled', 'schedule': '1h'}).
  2. Or delete the expired job and create a fresh one-shot with a new timestamp/duration.
  3. For recurring work, prefer interval/cron schedules — they recompute the next run from now on resume.

Example fix

# before
resume_job(job_id)  # one-shot time already passed while paused

# after
update_job(job_id, {"enabled": True, "state": "scheduled", "paused_at": None, "schedule": "30m"})
Defensive patterns

Strategy: try-catch

Validate before calling

from cron.jobs import compute_next_run

def resumable(job: dict) -> bool:
    return compute_next_run(job["schedule"]) is not None  # None => one-shot already past

Try / catch

from cron.jobs import resume_job
try:
    resume_job(job_id)
except ValueError as e:
    if "Cannot resume" in str(e):
        # give it a fresh future schedule instead of reviving the dead timestamp
        update_job(job_id, {"enabled": True, "state": "scheduled", "paused_at": None, "schedule": "30m"})

Prevention

When it happens

Trigger: pause_job(id) on a one-shot, waiting past its fire time (plus grace), then resume_job(id); also a one-shot paused minutes before firing and resumed just after.

Common situations: Pausing a daily report one-shot to skip a day, then resuming the next day expecting it to run; long pauses spanning the scheduled time.

Related errors


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