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
- Remove 'id' (and any other listed field) from the updates dict — never change a job's identity.
- To 'rename', update the 'name' field instead; to replace a job, create a new one and delete the old.
- 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
- Always send a diff to update_job, never the whole loaded job record.
- To change a job's identity, create a new job and delete the old one.
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
- Invalid cron job id for output path: {job_id!r}
- Preview mode — launching is disabled.
- no install root
- ${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest
- Remote connection is not ready yet. Try again in a moment.
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/0a3aa7db83dd4adc.
Report an issue: GitHub.