NousResearch/hermes-agent · error

${label} exited and ${dashboardIndexUrl(baseUrl)} is served

Error message

${label} exited and ${dashboardIndexUrl(baseUrl)} is served by a process we did not spawn; refusing its session token.

What it means

ValueError from update_job (cron/jobs.py:1865): the updates dict contains a field in _IMMUTABLE_JOB_FIELDS (currently just 'id'). 'id' is a filesystem path component under OUTPUT_DIR — allowing it to change would leak path-escape values into output writes/deletes, so mutation is blocked before any other update logic runs.

Source

Thrown at apps/desktop/electron/dashboard-token.ts:96

 */
function isForeignBackendToken({ servedToken, spawnToken, childAlive }) {
  return Boolean(servedToken) && servedToken !== spawnToken && !childAlive
}

/**
 * Resolve the token the backend actually serves, adopting benign drift and
 * failing loudly on a foreign backend. `childAlive` is a thunk so liveness is
 * sampled after the fetch, not before.
 */
async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, label = 'Hermes backend', ...options }) {
  const servedToken = await resolveServedDashboardToken(baseUrl, spawnToken, options).catch(error => {
    options.rememberLog?.(`[boot] could not read served dashboard token (${label}): ${error.message}`)

    return spawnToken
  })

  if (isForeignBackendToken({ servedToken, spawnToken, childAlive: childAlive() })) {
    throw new Error(
      `${label} exited and ${dashboardIndexUrl(baseUrl)} is served by a process we did not spawn; refusing its session token.`
    )
  }

  return servedToken
}

export {
  adoptServedDashboardToken,
  dashboardIndexUrl,
  DEFAULT_TOKEN_FETCH_TIMEOUT_MS,
  extractInjectedDashboardToken,
  fetchPublicText,
  isForeignBackendToken,
  resolveServedDashboardToken
}

View on GitHub (pinned to c896c09c42)

Solutions

  1. Remove 'id' (and any other listed field) from the updates dict — never change a job's identity.
  2. To 'rename', update the 'name' field instead; to replace a job, create a new one and delete the old.
  3. Strip immutable keys programmatically before calling update_job.

Example fix

# before
update_job(job["id"], job)  # full record, includes 'id'

# after
updates = {k: v for k, v in desired.items() if k != "id"}
update_job(job["id"], updates)
Defensive patterns

Strategy: validation

Validate before calling

IMMUTABLE = {"id"}

def sanitize_updates(updates: dict) -> dict:
    return {k: v for k, v in (updates or {}).items() if k not in IMMUTABLE}

update_job(job_id, sanitize_updates(desired_changes))

Try / catch

try:
    update_job(job_id, updates)
except ValueError as e:
    if "cannot be updated" in str(e):
        update_job(job_id, sanitize_updates(updates))  # strip immutable fields, retry
        raise

Prevention

When it happens

Trigger: update_job('a1b2c3d4', {'id': 'new-id'}); merging a full job dict back as the updates payload (it contains the id key); a generic 'save object' helper that round-trips every field including immutable ones.

Common situations: Load-modify-save patterns that submit the whole record instead of a diff; attempting to 'rename' a job by changing its id; bulk-import scripts reusing job dicts from another instance.

Related errors


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