agent-workflows: workspace-context-organization autonomous-ai-agents: hermes-agent computer-use devops: hermes-config-bulk-update, hermes-profile-management, holographic-memory, telegram-integration, webhook-subscriptions email: himalaya mcp: native-mcp, searxng-smart-search media: voice-systems, youtube-content mlops: local-vector-memory, qdrant-collection-management productivity: maps, notion, ocr-and-documents project-knowledge-base research: arxiv, blogwatcher, ecosystem-surveillance save-agents-md social-media: social-media-scraping, xurl software-development: agent-self-audit, simplify-code, spike, subagent-driven-development, systematic-debugging, test-driven-development, writing-plans user-response-style
34 KiB
name, description, version, author
| name | description | version | author |
|---|---|---|---|
| hermes-config-bulk-update | Bulk-update all Hermes config files (base + profiles) in one pass using execute_code. | 1.0.0 | Hermes Agent |
Hermes Config Bulk Update
When adding a shared setting (model, provider, MCP server, platform extra, display option), update ALL config files — base ~/.hermes/config.yaml + every ~/.hermes/profiles/*/config.yaml. Partial fixes are worse than no fix.
Template Script
Use execute_code with this pattern. The glob MUST explicitly exclude state-snapshots — see pitfall below.
import os, glob, yaml
home = os.environ['HOME']
configs = (
[f"{home}/.hermes/config.yaml"]
+ sorted(glob.glob(f"{home}/.hermes/profiles/*/config.yaml"))
)
configs = [p for p in configs if "state-snapshots" not in p]
for p in configs:
with open(p) as f:
cfg = yaml.safe_load(f)
# --- modify cfg here ---
with open(p, 'w') as f:
yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
profile = os.path.basename(os.path.dirname(p)) if '/profiles/' in p else 'BASE'
print(f"{profile}: done")
Reference Files
references/nous-research-api.md— Nous Research model name mapping (Ollama:cloud→ Nousprovider/model), config structure examples, auth status, and testing instructions.references/dgx-vllm-server.md— DGX vLLM server (10.0.0.6) connection details, current model, smoke tests, container info.references/model-temperature-configuration.md— Main inference temperature is NOT user-configurable in v0.17.0 (_fixed_temperature_for_modelonly handles Kimi/Arcee; PR #34219 unmerged). Lists per-subsystem temps that DO work (compression, vision, MoA), Ollama:cloudmodel behavior (honorsoptions.temperaturebut Hermes never sends it), and ranked workarounds (Modelfile bake, cherry-pick PR, proxy).references/profile-config-deep-clean.md— Single-profile config.yaml deep-clean + max optimization workflow: remove unused sections, max all limits, set full permissions, configure auxiliary models to local Ollama, test infrastructure endpoints, fill empty values, categorize intentional empties.references/searxng-search-requirement.md— Hard rule: ALWAYS usemcp_searxng_searxng_web_searchfor web searches, never the built-inweb_searchtool. User-corrected behavior.references/performance-tuning.md— Comprehensive Hermes config performance/quality review: critical fixes (web.extract_backend), streaming, tool output limits, session pruning, checkpoints, browser engine, smart model routing, delegation depth, prompt caching, watch items, and bulk-patch YAML paths with verification commands.references/local-ollama-profile-model-selection.md— When setting a profile model to an Ollama-hosted model, query the live local catalog, pick only from available models, and verify/correct the profile'sbase_urlso it points at local Ollama rather than an inherited remote vLLM endpoint.references/model-picker-audit.md— How to reproduce exactly what the/modelpicker shows for a profile from execute_code (no interactive session needed), why each row appears (credential detection logic in model_switch.py), and fixes for phantom rows from stale auth.json credential_pool entries and the always-present MoA virtual row.references/workspace-seeding.md— After addingworkspace:keys to all profile configs, create matching directories on the host and seed each with an AGENTS.md so the agent has workspace context from the first session.references/workspace-symlink-bridge.md— When all profile configs use/workspace/<name>but actual dirs are at/home/n8n/workspace/<name>/, createsudo ln -s /home/n8n/workspace /workspaceto bridge them. One symlink fixes all 16 profiles.references/checkpoint-cleanup.md— Detect and remove stale cross-profile checkpoints that leak context between profiles (e.g., telegram profile checkpoint pointing to comfy profile dir).references/home-directory-organization.md— When the home directory accumulates stray project directories and files, classify each item (active deployment, runtime data, project dir, stale artifact) and move/delete accordingly, updating all config references.references/hardcoded-context-length-override.md— Hermes hardcoded context-length table inmodel_metadata.pyoverrides vLLM's livemax_model_len; fix pattern withcontext_lengthin model config + cache clearing.references/iteration-turn-limits.md— When an agent stops early ("stopped after N iterations"), the bottleneck can be in Hermes config (agent.max_turns,delegation.max_iterations,delegation.max_turns), vLLM server args, or nuntius-mcp config. Check ALL layers — don't assume it's the server. Includes profile-by-profilemax_turnsbreakdown.references/toolset-cleanup.md— Removing non-functional toolsets (moa, rl) that produce "Unknown toolsets" warnings at startup. Line-filtering pattern for safe config line removal without YAML parse/dump.references/moa-configuration.md— MoA (Mixture of Agents) config structure, provider ID format, preset management, limits (8 concurrent refs, unlimited presets), and pitfalls (dual config locations, flat compat fields).references/memory-provider-switch.md— Switchingmemory.provider(e.g., hindsight ↔ supermemory) across all profiles: per-profile anchor pattern, key-order variance, double-validation workflow.references/personality-soul-system.md— Hermes personality architecture (SOUL.md identity → display.personality default → /personality session overlay), 14 built-in presets, custom personality config, bulk personality change across all configs, per-profile SOUL.md customization, and what can/cannot be removed.scripts/verify_provider_fleet.py— Assert every config (base + all profiles) reports the samememory.provider. Use after any provider bulk switch. Run withpython3 <skill_dir>/scripts/verify_provider_fleet.py <expected_provider>(exits 1 if any mismatch).
Common Config Paths
| Feature | YAML path | Example |
|---|---|---|
| Custom providers | custom_providers (list) |
{"name":"Ollama","base_url":"...","api_key":"m","model":"..."} |
| Model aliases | model_aliases (dict) |
{"deep": {"model":"...","provider":"custom",...}} |
| MCP servers | mcp_servers (dict) |
{"searxng": {"command":"npx","args":["-y","mcp-searxng"],"env":{...}}} |
| Telegram extras | platforms.telegram.extra (dict) |
{"rich_messages": true} |
| Runtime footer | display.runtime_footer (dict) |
{"enabled": true, "fields": ["model","context_pct","cost"]} |
| Web backend | web (dict) |
{"backend": "searxng", "searxng_url": "http://..."} |
Sticky Default Profile (~/.hermes/active_profile)
When hermes is invoked without -p/--profile, it reads ~/.hermes/active_profile to determine which profile to use. If the file is absent or empty, it falls back to "default" (base ~/.hermes/).
Check: cat ~/.hermes/active_profile
Set: hermes profile use <name> (writes the file) or hermes profile use default (deletes the file, restoring base ~/.hermes/)
Pitfall — Docker WebUI sets wrong sticky default: When the WebUI container had its own profiles directory and those were copied to the local filesystem during a mount fix, the sticky default may have been set to a Docker-origin profile (e.g., telegram). Running plain hermes then targets that profile instead of the intended CLI profile. Fix: hermes profile use general or hermes profile use default.
Include in "update ALL locations" checklist: When doing a bulk config sweep, also verify ~/.hermes/active_profile points to the intended profile. A config alignment is incomplete if the sticky default routes to a stale or wrong profile.
Profile Alignment (Template-Copy Pattern)
When the user wants all profiles to share the same model/provider/alias configuration, pick one profile as the template (usually the active CLI profile, e.g. general) and copy its model, custom_providers, and model_aliases sections to every other config — base + all profiles. Use copy.deepcopy to avoid shared references.
import os, glob, yaml, copy
home = os.environ['HOME']
template_profile = "general"
with open(f"{home}/.hermes/profiles/{template_profile}/config.yaml") as f:
template = yaml.safe_load(f)
template_model = copy.deepcopy(template['model'])
template_cp = copy.deepcopy(template['custom_providers'])
template_aliases = copy.deepcopy(template['model_aliases'])
configs = (
[f"{home}/.hermes/config.yaml"]
+ sorted(glob.glob(f"{home}/.hermes/profiles/*/config.yaml"))
)
configs = [p for p in configs if "state-snapshots" not in p and template_profile not in p]
for p in configs:
with open(p) as f:
cfg = yaml.safe_load(f)
cfg['model'] = copy.deepcopy(template_model)
cfg['custom_providers'] = copy.deepcopy(template_cp)
cfg['model_aliases'] = copy.deepcopy(template_aliases)
# Remove stale providers: {} (write-only bug — Docker artifact)
if 'providers' in cfg and cfg['providers'] == {}:
del cfg['providers']
with open(p, 'w') as f:
yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
Verification after alignment: Check every config for provider, default, base_url, custom_providers count, and model_aliases count. All must match the template exactly.
Pitfall — providers: {} Docker artifact: Docker WebUI containers sometimes write providers: {} into profile configs. This is the write-only bug — the key is empty and useless. Delete it during alignment. The correct provider config lives in custom_providers: list and model.provider. Run this cleanup even when no other alignment is needed — it's a standalone hygiene check:
grep -rn 'providers: {}' ~/.hermes/profiles/*/config.yaml ~/.hermes/config.yaml
If any found, remove with hermes config set providers null --profile <name> or via the alignment script's deletion step.
Pitfall — cp -r lands files in wrong directory: cp -r source/plugins/<plugin_name> target/plugins/ copies the CONTENTS of the plugin dir into plugins/, not the directory itself. Always mkdir -p target/plugins/<plugin_name> first, then cp -r source/plugins/<plugin_name>/* target/plugins/<plugin_name>/. Same pattern applies to any plugin or skill directory replication.
Mem0 JSON Bulk Update (LEGACY, deprecated 2026-06-29)
The original mem0 plugin required a mem0.json file per profile. All 20 profiles have migrated from mem0_oss to hindsight, so this section is preserved for historical reference only. Do not run this script — mem0.json files no longer exist.
The pattern (JSON bulk update) is still useful for any JSON config file (env files, plugin manifests, etc.). The original script targeted mem0.json files and updated ollama_base_url to point at a new Ollama endpoint:
# LEGACY 2026-06-29 — mem0 no longer in use, but pattern is reusable for any JSON
import os, json
HERMES = os.path.expanduser("~/.hermes")
updated = 0
for root, dirs, files in os.walk(HERMES):
if "state-snapshots" in root:
continue
for f in files:
if f.endswith(".json") and "config" in f: # was: if f == "mem0.json"
fpath = os.path.join(root, f)
with open(fpath) as fh:
data = json.load(fh)
# ... modify data ...
with open(fpath, "w") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
updated += 1
print(f"Updated {updated} config files")
.env Bulk Update (env vars across all profiles)
Some env vars must be in ~/.hermes/.env files, NOT just config.yaml. The built-in web_search tool's availability check (_has_env() in tools/web_tools.py) reads from os.environ → ~/.hermes/.env via get_env_value() — it does NOT read config.yaml keys like web.searxng_url. If an env var is only in config.yaml, the tool silently fails.
When .env is needed (not just config.yaml):
SEARXNG_URL— built-inweb_searchreads from.env, notweb.searxng_urlin config.yaml- Any env var consumed by
_has_env()or_env_value()intools/web_tools.py - API keys for providers that check
os.environdirectly
Bulk .env update pattern:
# Check which profiles are missing the env var
for p in ~/.hermes/profiles/*/; do
grep -l SEARXNG_URL "$p/.env" 2>/dev/null || echo "MISSING: $(basename $p)"
done
# Add to all missing profiles (base + profiles)
echo 'SEARXNG_URL=http://10.0.0.8:8888' >> ~/.hermes/.env
for p in ai comfy dgx llm people research social tts; do
echo 'SEARXNG_URL=http://10.0.0.8:8888' >> ~/.hermes/profiles/$p/.env
done
# Verify all 16 locations now have it
for p in ~/.hermes/profiles/*/; do
grep -c SEARXNG_URL "$p/.env" 2>/dev/null || echo "0"
done
grep -c SEARXNG_URL ~/.hermes/.env
Pitfall — config.yaml alone is not enough for env-var-gated tools: The web section in config.yaml (web.backend, web.searxng_url) configures WHICH backend to use, but the backend's availability check reads the env var from .env. Both must be set. The MCP SearXNG tools have their own separate env block in mcp_servers.searxng.env and are unaffected by .env — they work even when the built-in tool is broken.
Verification after .env update: Start a new session (/reset) — env vars are read at startup. Test with web_search("test") to confirm results return.
Hindsight Setup Across Profiles (canonical, since 2026-06-29)
Hindsight is configured in a single file (~/.hermes/hindsight/config.json) shared by all profiles. Each profile only needs memory.provider: hindsight in its config.yaml — no JSON replication, no per-profile plugin dir.
Setup steps:
- Write
~/.hermes/hindsight/config.jsononce (seedevops/hindsight-memory-setup/SKILL.md) - Set
memory.provider: hindsightin every profile'sconfig.yaml(usereferences/memory-provider-switch.mdfor the bulk-update playbook) - Verify with
scripts/verify_provider_fleet.py— asserts every profile reportsmemory.provider: hindsight
Why this is simpler than mem0:
- Single config file vs 16
mem0.jsoncopies - No per-profile plugin copy (Hindsight plugin is bundled with
hermes-agent) - No pip install per profile (no
mem0ai, noollama) - WebUI container needs no setup (reads host's
~/.hermes/hindsight/config.jsonvia volume mount)
Mem0 OSS Replication Across Profiles (LEGACY, deprecated 2026-06-29)
The mem0_oss plugin was the canonical memory provider until 2026-06-29. This section is preserved for historical reference. Do not run these commands — mem0.json files and plugins/mem0_oss/ directories no longer exist.
The original procedure required three artifacts per profile:
mem0.json— the Mem0 config file (Qdrant host, Ollama models, collection name)plugins/mem0_oss/— the plugin directory with__init__.pymemory.provider: mem0_ossinconfig.yaml
None of these inherited from base ~/.hermes/ — each profile needed its own copy. The original replication commands (now obsolete):
# LEGACY 2026-06-29 — these files no longer exist
for profile in general telegram; do
cp ~/.hermes/profiles/telegram/mem0.json ~/.hermes/profiles/$profile/mem0.json
done
cp ~/.hermes/profiles/telegram/mem0.json ~/.hermes/mem0.json
for profile in general; do
mkdir -p ~/.hermes/profiles/$profile/plugins/mem0_oss
cp -r ~/.hermes/profiles/telegram/plugins/mem0_oss/* ~/.hermes/profiles/$profile/plugins/mem0_oss/
done
See hermes-webui-docker/references/mem0-webui-verification.md for the original end-to-end verification procedure (also LEGACY).
Per-Profile Workspace Configuration
When the WebUI needs each profile to have its own workspace directory (so switching profiles in the UI switches the file browser), add a workspace: key to every config.yaml. The WebUI's _profile_default_workspace() in api/workspace.py checks config.yaml for workspace or default_workspace keys, then falls back to terminal.cwd. Without an explicit key, all profiles share the global /workspace mount.
Pattern
import os, glob, re
home = os.environ['HOME']
configs = [f"{home}/.hermes/config.yaml"] + sorted(glob.glob(f"{home}/.hermes/profiles/*/config.yaml"))
configs = [p for p in configs if "state-snapshots" not in p]
def profile_name(path):
if '/profiles/' in path:
return os.path.basename(os.path.dirname(path))
return 'default'
for cfg_path in configs:
name = profile_name(cfg_path)
with open(cfg_path) as f:
content = f.read()
# Insert workspace key before terminal: section
new_content = re.sub(
r'^(terminal:)',
f'workspace: /workspace/{name}\n\\1',
content,
flags=re.MULTILINE
)
with open(cfg_path, 'w') as f:
f.write(new_content)
Pitfalls
- Use absolute
/workspace/<name>, not~/workspace/<name>. Inside the Docker container,~expands to/home/hermeswebui, not/workspace. The WebUI's_profile_default_workspace()calls_resolve_path()which doesos.path.expanduser()—~/workspace/generalbecomes/home/hermeswebui/workspace/generalwhich doesn't exist. Use/workspace/<name>so it resolves correctly inside the container. - Create the workspace directories on the host first. The WebUI's
_profile_default_workspace()checksp.is_dir()— if the directory doesn't exist, it falls through to the next fallback. Create~/workspace/<name>for every profile before adding the config key. - The WebUI uses TLS (thread-local storage) for per-request profile switching.
set_request_profile(name)sets the active profile for the current request thread, and_profile_default_workspace()reads that profile's config.yaml. Testing withHERMES_HOMEenv var alone won't work — useset_request_profile()to simulate actual WebUI behavior. re.subon raw file content preserves all formatting and key order. Prefer this overyaml.load/yaml.dumpfor simple insertions — YAML dump can reorder keys and mangle long string values.
Verification
# Host-side: all configs have the key
for p in default ai automation coding comfy dgx experimental general llm minimal people personal research telegram tts work; do
cfg="/home/n8n/.hermes/profiles/$p/config.yaml"
test "$p" = "default" && cfg="/home/n8n/.hermes/config.yaml"
grep "^workspace:" "$cfg"
done
# Container-side: WebUI resolves per-profile
docker exec hermes-webui /app/venv/bin/python3 -c "
from api.profiles import set_request_profile, clear_request_profile
from api.workspace import _profile_default_workspace
for p in ['general', 'telegram', 'ai', 'comfy', 'default']:
set_request_profile(p)
ws = _profile_default_workspace()
print(f'{p}: {ws}')
clear_request_profile()
"
# Container-side: directories exist
docker exec hermes-webui ls -d /workspace/{default,ai,automation,coding,comfy,dgx,experimental,general,llm,minimal,people,personal,research,telegram,tts,work}
Pitfalls
provider: customfor ALL self-hosted endpoints (vLLM, Ollama, llama.cpp, etc.). The correct provider ID iscustom, NOTopenai. Theopenaiprovider ID is reserved for the official OpenAI API withOPENAI_API_KEY/openai-api. Usingprovider: openaiwith a self-hosted base_url may appear to work in some cases but is incorrect per Hermes docs and will cause routing/auth issues. The docs canonical form:model: default: nvidia/nemotron-3-super provider: custom base_url: http://10.0.0.6:8000/v1 api_key: dummyyaml.dumpwithsort_keys=Falsepreserves existing key ordercfg.setdefault('key', {})creates nested dicts safely without overwriting- Gateway restart from inside gateway is blocked — user must run
hermes gateway restartfrom shell or/restartin chat - Config changes take effect on next gateway cycle, not immediately
- The
providers:dict IS functional — it actively overridesmodel.base_url. Whenproviders:contains an entry matching the active provider name, that entry'sbase_urlwins overmodel.base_url. This is the #1 cause of "model not found" 404s when the model exists on the intended endpoint but the request goes to the wrong server. Useproviders: 'null'to disable this override (matching the general profile pattern). For multi-provider setups, register secondary providers viahermes config set providers.<name>.*— seereferences/multi-provider-profile-setup.md.\n-providers: {}(empty dict) is a Docker WebUI artifact, not a bug. The WebUI container sometimes writesproviders: {}into profile configs. It's harmless clutter — an empty dict has no entries to overridemodel.base_url. Delete it during cleanup, but don't confuse it with the functionalproviders:dict that contains actual provider entries. The functionalproviders:dict (with named entries) IS the override mechanism — see pitfall above.\n- Auth.jsonbase_urloverrides config.yamlmodel.base_url. The credential pool entry for the active provider carries its ownbase_url. Even when config.yaml has the correct endpoint, if auth.json's entry points elsewhere, requests go to the wrong server. Always verify auth.json after config changes:python3 -c "import json; d=json.load(open('auth.json')); [print(f'{k}: {e.get(\"base_url\")}') for k,v in d['credential_pool'].items() for e in v]"
Multi-Provider Setup (Ollama + External APIs)
The user's setup runs two provider stacks simultaneously: a local Ollama proxy for :cloud models, and direct API endpoints (e.g., Nous Research) accessible via model aliases.
Ollama Proxy (default)
All :cloud model traffic routes through http://localhost:11434/v1 with api_key: m. This is the working default provider. Model names use the model:cloud format (e.g., minimax-m3:cloud, glm-5.1:cloud).
External Direct APIs (via model aliases)
External APIs like Nous Research are configured as custom_providers entries + model_aliases so users can switch with /model nous-m3. Each alias overrides base_url, provider, and api_key for that model only — the default stays on Ollama.
Key naming convention: Ollama uses model:cloud; direct APIs use provider/model (e.g., minimax/minimax-m3). Mixing formats across endpoints causes 404s.
Adding a new external provider
-
Test the chat endpoint first — model listing (
GET /v1/models) succeeding does NOT mean chat completions work. Test with:curl -s -w '\nHTTP_CODE:%{http_code}' <base_url>/chat/completions \ -H "Authorization: Bearer <api_key>" \ -H "Content-Type: application/json" \ -d '{"model":"<model_id>","messages":[{"role":"user","content":"hi"}],"max_tokens":5}'A 200 confirms endpoint+key+model work. A 401/404 means broken — do NOT write to all 9 configs.
-
Add custom_providers entries for each model on the external API (one entry per model with its correct
provider/modelID). -
Add model_aliases with a short prefix (e.g.,
nous-m3,nous-deep) pointing to the external API's base_url, provider, and api_key. -
Add credential pool entry to
auth.jsonfor the external provider (keycustom:<hostname>). -
Update ALL 9 config files — base + every profile. Partial updates are worse than no updates.
-
Clear stale caches — delete
models_dev_cache.json,model_catalog.json,.skills_prompt_snapshot.json,context_length_cache.yamlfrom~/.hermes/and profile dirs.
Pitfalls
- Never change the default
base_urlaway from a working endpoint. The default provider must stay on Ollama; external APIs go in aliases only. - Always validate the model name against
GET /v1/modelsbefore writing to configs. The Nous API does NOT resolve short/alias model names —nvidia/nemotron-3-superreturns 404 while the full IDnvidia/nemotron-3-super-120b-a12bworks. Always hit the models endpoint and grep for your target, then use the exact ID string from the response. Writing a wrong model name to all 9 configs bricks every profile. - A 401 on chat/completions means the key is invalid/expired/blocked. The fix is a new key from the provider's dashboard, not a config change. But verify the key isn't just truncated —
read_fileand terminal output may displaysk-nou...M5nywith literal dots instead of the full key, making a valid 40-char key look like a 13-char string. yaml.dumpwithsort_keys=Falsepreserves key order;cfg.setdefault('key', {})creates nested dicts safely.- Prefer
str.replace()on raw file content overyaml.load/yaml.dumpfor config edits with API keys. YAML dump can mangle long string values, and regex on raw text is fragile when the replacement string contains special characters. Use simplecontent.replace(old, new)for targeted swaps — it preserves all surrounding formatting and key order exactly. - Always verify API key writes with byte-level checks, not display output.
read_fileandgrepmay truncate long keys at display time. Usepython3 -c "with open(path) as f: ... ; print(len(key))"to confirm the actual length stored in the file matches expectations. - Gateway restart from inside gateway is blocked — user must
/restartorhermes gateway restart. - Config changes take effect on next session, not mid-conversation.
- Check Hermes docs BEFORE writing configs when unsure of field values. Don't guess provider IDs, field names, or syntax from grep output. The docs at https://hermes-agent.nousresearch.com/docs/integrations/providers are authoritative. A quick web_search/web_extract is cheaper than writing wrong values to 9 files then fixing them.
- The active profile's config.yaml is write-protected from
patch()andexecute_code. If the current session is running under profiletelegram, the agent cannot editprofiles/telegram/config.yamlvia those tools. Workaround: useterminalwithsed -ifor that one file, or usepython3 -cwithyaml.safe_load/yaml.dump(which runs in terminal, bypassing the patch guard). Then verify. - Removing an MCP server block with
patch()can leave orphaned lines. When the server block is not the last entry inmcp_servers:, the next server'senv:/timeout:lines may be left behind as orphans, corrupting the YAML. Always read the surrounding context (5+ lines after the block) before crafting theold_string, and verify withpython3 -c "import yaml; yaml.safe_load(open(path))"after the patch. If the YAML is corrupted, fix with a second targeted patch to remove the orphaned lines. - Auth pool entries (
custom_providerslist) also carrymodel:fields. When updating a model alias, grep for the OLD model string across the entire file — the alias block AND the auth pool entry both need updating. Always finish with a negative grep for the old value to confirm zero stale references remain. state-snapshots/is OFF-LIMITS during bulk updates.~/.hermes/state-snapshots/pre-update-*/and~/.hermes/profiles/*/state-snapshots/pre-update-*/are immutable pre-update backups created byhermes update. Never include them in aglob('~/.hermes/**/config.yaml')sweep — glob will descend into them and your YAML writes will silently mutate frozen snapshots. Filter them out explicitly:If you accidentally modify one, revert by reloading the snapshot and setting the field back, or restore fromconfigs = [] for p in glob.glob(f"{home}/.hermes/config.yaml") + glob.glob(f"{home}/.hermes/profiles/*/config.yaml"): if "state-snapshots" not in p: configs.append(p)~/.hermes/backups/pre-update-*.zip. Snapshots are NOT git-tracked, so there's nogit checkoutfallback.- "Unknown toolsets: moa, rl" warning at startup means stale toolset entries in config. The
moaandrltoolsets are off-by-default and their tool modules don't exist. Removing them from thetoolsets:list silences the warning. This does NOT disable the/moaslash command — MoA is a core feature, not a toolset. Use line filtering (not YAML parse/dump) for safe removal — seereferences/toolset-cleanup.md. - Model temperature is NOT user-configurable for the main inference path (v0.17.0). Do NOT tell the user they can set
model.temperature— that key does not exist in the config schema (confirmed via DEFAULT_CONFIG introspection)._fixed_temperature_for_model()(agent/auxiliary_client.py:287-306) only returns OMIT_TEMPERATURE for Kimi/Moonshot and 0.5 for Arcee Trinity; everything else gets no temperature field → provider default. PR #34219 would addmodel.temperaturebut is open/unmerged. Per-subsystem temps DO work:compression.summarization.temperature(default 0.3),auxiliary.vision.temperature(0.1), MoA preset temps. For Ollama:cloudmodels, the backend honorsoptions.temperaturebut Hermes never sends it — the fastest fix is a Modelfile (FROM <model>:cloud+PARAMETER temperature <x>,ollama create). Seereferences/model-temperature-configuration.mdfor the full stack analysis and ranked workarounds. - NEVER assume model selection for cron jobs. When creating agent-driven cron jobs, always ask the user which model/provider to use. Do not default to the session model or pick one yourself. Script-only cron jobs (
no_agent=true) don't need a model. This applies to any cron job that uses an LLM — the model must be explicitly chosen by the user and pinned via themodelparameter oncronjob(action='create')orcronjob(action='update'). discover_modelson acustom_providersentry controls the/modelpicker source. Whentrue(or absent with an api_key set), the picker calls<base_url>/v1/modelsand uses the live response. Whenfalse, the picker is locked to the staticmodels:dict under that entry. The picker logic lives inhermes_cli/model_switch.py(should_probeblock) and the live fetch caches to<HERMES_HOME>/provider_models_cache.json(deleted on--refresh). Implication: if a user says "the /model picker only shows N models for provider X", the fix is almost always flippingdiscover_models: false→trueon that custom_provider entry across all configs — not adding more entries to the staticmodels:list. Always do a livecurlagainst<base_url>/v1/modelsfirst to confirm the endpoint actually returns more than the static list claims, before writing to 9 files.- When an agent stops early ("stopped after N iterations"), check
agent.max_turnsin the ACTIVE PROFILE first, not the server. vLLM has no built-in iteration limit — it serves requests until the client stops. nuntius-mcp has its ownmax_iterationsbut is a separate system. The #1 culprit is the profile'sagent.max_turnsoverriding the base config's higher value. If the stopped-at number is exactly 60, 50, or 20, it matches a Hermes default and confirms the limit is Hermes-side. Seereferences/iteration-turn-limits.mdfor the full multi-layer checklist. - Reasoning models on vLLM need
extra_bodyto disable thinking. Models likeqwen3.6-35b-a3b-uncensoredon vLLM put output in thereasoningfield withcontent: nullby default. The/chat/completionsendpoint returns HTTP 200 but the message content is empty. Fix: addextra_body: {chat_template_kwargs: {enable_thinking: false}}to the model entry incustom_providers. Verify with a direct curl before writing to all configs — test both with and without the flag to confirm the endpoint accepts it. - Hermes hardcoded context-length table overrides vLLM's
max_model_len. When a custom provider model's name matches a substring inagent/model_metadata.py's_HARDCODED_CONTEXT_LENGTHSdict, that hardcoded value wins over the live/v1/modelsresponse. Example:qwen3.6-35b-a3b-uncensoredon vLLM reportsmax_model_len: 262144but Hermes' catch-all"qwen": 131072matches first, so Telegram/CLI show 131K instead of 262K. Fix: addcontext_length: <real_value>to the model entry undercustom_providers→models→<model_name>. Then clearcontext_length_cache.yamlfrom~/.hermes/and all profile dirs so Hermes re-probes. Verify withcurl -s http://<host>:<port>/v1/models | grep max_model_lento get the real value before writing configs. - Model aliases don't resolve in
hermes chat -q -m <alias>(only in-zoneshot mode). TheDIRECT_ALIASESdict (populated frommodel_aliases:in config.yaml) is checked byrun_oneshot()but NOT by the interactive CLI path (HermesCLI.__init__→_ensure_runtime_credentials). Runninghermes chat -q -m epycsends the literal alias name "epyc" as the model to the default provider (Ollama), which returns 404. Workaround: use the full model ID with explicit provider/base_url, or use/model epycin an interactive session (which does resolve aliases). The-zoneshot flag works correctly with aliases. - Stale
auth.jsoncredential_pool entries cause phantom rows in the/modelpicker. A built-in provider (e.g.huggingface) appears in the picker when itscredential_poolentry exists in the profile'sauth.json, even if the corresponding env var (HF_TOKEN) is NOT in.env. The credential pool'shas_credentials()check makeslist_authenticated_providers()include the row. Models are sourced fromprovider_models_cache.json. Fix: remove the stale slug fromauth.jsoncredential_poolAND fromprovider_models_cache.json. To audit exactly what a profile's picker shows without an interactive session, usebuild_models_payload(load_picker_context())withHERMES_HOMEset to the PROFILE directory (not~/.hermes) — seereferences/model-picker-audit.mdfor the full reproduction recipe and all row-source logic. - Bulk alias removal: the anchor after
model_aliases:varies across profiles. When usingpatch()to strip all aliases except dgx/epyc, thenew_stringmust include the line immediately after the aliases block as an anchor. But that line differs per profile: some havenetwork:, some havemodel_catalog:, some haveplatform_toolsets:. If thenew_stringhardcodes the wrong anchor (e.g.,model_catalog:for a profile that hasplatform_toolsets:next), the replacement silently eats the real section header. Fix: either (a) useexecute_codewithpatch()in a loop and verify each profile withgrepafter, or (b) use a two-pass approach — first strip aliases with a generic anchor, then fix any collateral damage. Always run a finalgrep -q "name: Nous\|nous-"sweep across all profiles to confirm zero stale references remain.