> **⚠️ PITFALL: Exactly one config watcher should be active**  
> Do not run both a user-level and a system-level `hermes-config.path` against the same `~/.hermes/config.yaml`. Duplicate watchers produce duplicate `model-switch.log` lines and can race service swaps. Check with `systemctl --user is-active hermes-config.path` and `systemctl is-active hermes-config.path`. Keep the watcher architecture used by the installation and disable the duplicate. In Ray's current setup, the **user** watcher is the intended active watcher; the duplicate system watcher was disabled.

## Three-Layer Defense + Notification Handoff

Bridges the gap between Hermes' `/model` command and local llama.cpp servers (which need a systemd service stop/start).

| Layer | Trigger | Service swap latency | Notification latency | Cost |
|-------|---------|---------------------|---------------------|------|
| **1. Gateway persistence** | `/model` picker in chat writes config.yaml | Instant | ~10s (cron tick) | 0 tokens |
| **2. Path watcher** | `config.yaml` write (`PathModified`) | Instant + API wait | ~10s (cron tick) | 0 tokens |
| **3. Cron watchdog** | Every 30 min | ≤30 min | N/A (only on restart) | 0 tokens |

Layer 1 catches `/model` switches from the chat picker — the gateway writes **all three** model config fields atomically (`model.default`, `model.provider`, `model.base_url`) via `save_config()` at `gateway/run.py:8979-9002`. This triggers the path watcher. Layer 2 handles the actual service swap. Layer 3 catches crashes/stops between switches.

**Important:** The gateway does NOT call `quick-start-model.py` directly anymore. It relies on the path watcher + `auto-model-switch.py` to handle the service lifecycle. The gateway code strips `base_url` from the session override for local models (lines 8963-8972) so the agent resolves it from config.yaml, which `auto-model-switch.py` updates with the correct per-model port.

### The Base URL Bug (Critical Pitfall)

Every script that swaps services MUST also update model.base_url in config.yaml. The custom:local provider definition uses base_url http://127.0.0.1:8081/v1 (port 8081), but each model runs on a different port. If a script starts phi-4 on port 8088 without updating base_url, Hermes still points to 8081 -> api failed after 3 retries.

The fix is always the same: update model.base_url to http://127.0.0.1:{port}/v1 after starting the service.

### The custom_providers Base URL (Critical Pitfall)

custom_providers[0].base_url MUST be set, or the entire local provider is invisible in /model picker. The config parser _normalize_custom_provider_entry() (hermes_cli/config.py:2729) returns None when base_url is missing or invalid. This silently drops the entire custom provider from the picker - zero local models shown, no error message.

This base_url is a grouping key for the picker, NOT the runtime endpoint. It doesn't need to match the currently active model's port. A fixed value like http://127.0.0.1:8081/v1 works for all models under the same provider group. The runtime URL comes from model.base_url which gets updated by auto-model-switch.py on every switch.

**How it gets lost — write_model_yaml bug:**
`auto-model-switch.py`'s `write_model_yaml()` replaces the first `base_url:` line in config.yaml (under `model:`) with the new port. The *second* `base_url:` line (inside `custom_providers:`) must be PRESERVED, not dropped. Older versions of the script had:
```python
else:
    continue  # ← DROPS the second base_url line!
```
This silently kills the /model picker every time auto-model-switch.py runs. The fix is:
```python
else:
    # Preserve any subsequent base_url (e.g. inside custom_providers)
    new_lines.append(line)
```
If you add new swap scripts, verify they don't have this bug.

**Config structure (requirements for the picker to show local models):**
```yaml
custom_providers:
- name: local
  base_url: http://127.0.0.1:8081/v1    # REQUIRED, any valid URL
  models:
    phi-4-Q4_K.gguf:
      context_length: 16384   # Phi-4 native context; do not fake 65K with rope-scale
    gemma-3-12b-it-Q4_K_M.gguf:
      context_length: 131072
```

Picker filter logic (in list_picker_providers at hermes_cli/model_switch.py:1758-1777):
- has_models = bool(p.get("models"))
- is_custom_endpoint = bool(p.get("is_user_defined")) and bool(p.get("api_url"))
- if not has_models and not is_custom_endpoint: continue

A custom provider passes if it has models OR is_user_defined + api_url. Both require the base_url to be present in the original config entry — without it, _normalize_custom_provider_entry() returns None before the entry reaches the picker.

**Diagnosis:**
```bash
python3 -c "import yaml
with open('/home/rurouni/.hermes/config.yaml') as f:
    cfg = yaml.safe_load(f)
cp = cfg.get('custom_providers', [])
for p in cp:
    print(f'name={p.get(\"name\")} base_url={p.get(\"base_url\", \"MISSING!\")} models count={len(list(p.get(\"models\", {}).keys()))}')"
```

**The two base_urls are independent:**
- model.base_url — runtime endpoint, updated by scripts on every model swap
- custom_providers[0].base_url — picker grouping key, should stay fixed at a default port

### State/Provider Drift Bug

The `auto-model-switch.py` state guard (lines 387-418) only checks `model.default` against last-good-state, NOT `model.provider`. This means:

1. `hermes config set model.default phi-4-Q4_K.gguf` — path watcher fires, auto-switch starts phi-4, saves state `(phi-4, deepseek)` if provider was still deepseek
2. `hermes config set model.provider custom:local` — path watcher fires, auto-switch sees `model.default` = phi-4 and state = phi-4 → "state unchanged, healthy" → skips, leaving provider wrong in state
3. On the next REAL switch, the stale provider in state can cause the guard to skip unexpectedly

**Impact:** The service runs fine, but the state file says the wrong provider. Not functionally breaking, but can cause confusion when diagnosing the system. The gateway's picker path avoids this by writing all three fields atomically.

## Notification Delivery (Cross-Platform)

All three layers deliver notifications through a **file-handoff + cron dispatch** system that works on any platform (Telegram, Discord, etc.):

1. Event produces a message → written to `~/.hermes/.model-switch-notify`
2. Trigger `hermes cron run <watchdog-id>` → queues for next scheduler tick (~10s)
3. Cron scheduler ticks → runs `model-health-watchdog.py`
4. Watchdog reads file, prints content to stdout
5. Cron delivery system auto-resolves origin platform and sends the message

No hardcoded bot tokens or chat IDs. Hermes cron handles delivery to whatever platform you're on.

### Path Watcher Notification (auto-model-switch.py)

`auto-model-switch.py` (triggered by path watcher) writes `write_notification(target, action)` to `.model-switch-notify`. The 30-min watchdog picks it up on its next tick.

```text
🔀 **Model Switch**
━━━━━━━━━━━━━━━
**Event:** switched
**Model:** Gemma-3-12B
**Port:** 8089
**VRAM:** 7911 MB / 11264 MB
**Context:** 131072K
**Time:** 02:45:00 PM PDT
```

**VRAM is read AFTER the model loads.** `swap_to()` in auto-model-switch.py now waits for the API to respond (up to 60s poll on `/v1/models`) before calling `write_notification()`. This ensures VRAM shows the model's actual usage (e.g., 8821 MB) instead of 2 MB (unloaded system state).

## Architecture

```
Gateway: /model picker in chat
        │
        ▼ (always persists all 3 model fields)
gateway/run.py:8979-9002 (save_config)
        │
        ▼
~/.hermes/config.yaml  ─── hermes-config.path ─── auto-model-switch.py
                                        │        (Layer 2: on config write)
                                        ▼
                             1. Stop old service (if any)
                             2. Start target service
                             3. Wait for API (up to 60s)
                             4. Read VRAM
                             5. Write .model-switch-notify
                             6. Save state
                             
     On rollback (start failed): write config.yaml to revert to previous model
     (safe because the lock file prevents re-entry while rolling back)

        ▼
cron: every 30min, no_agent
model-health-watchdog.py
  1. Check .model-switch-notify → print + delete
  2. Check service health → restart if dead → write notify
```

## Layer 1: Gateway Persistence — Instant on `/model` Picker

The gateway writes to config.yaml at `gateway/run.py:8979-9002` whenever the user selects from the `/model` picker (Telegram/Discord interactive picker):

```python
_per_cfg = yaml.safe_load(f) or {}
_per_model_cfg = _per_cfg.setdefault("model", {})
_per_model_cfg["default"] = result.new_model
_per_model_cfg["provider"] = result.target_provider
if result.base_url:
    _per_model_cfg["base_url"] = result.base_url
from hermes_cli.config import save_config as _save_config
_save_config(_per_cfg)
```

This writes ALL three model fields atomically (not one at a time). The `save_config` function uses `atomic_yaml_write` — creates a temp file, dumps via `yaml.dump()`, then renames atomically. This preserves every config section, including `custom_providers`.

**Text-based `/model` path (without picker)** — at `gateway/run.py:9084-9153` — only persists if `--global` flag is used (line 9154). Without `--global`, it's session-only and does NOT update config.yaml. The user sees "This change is session-only" in the confirmation.

### Stripping base_url from session override

The gateway intentionally removes `base_url` from the session override for local models (lines 8963-8972 of `run.py`):
```python
if (_tp.startswith("custom:") or _tp == "custom") and (
    "localhost" in _bu or "127.0.0.1" in _bu
):
    _ovr.pop("base_url", None)
```
This causes the agent to resolve `base_url` from config.yaml on each new turn — so when `auto-model-switch.py` updates config.yaml with the correct per-model port, the agent picks it up automatically. Without this, the agent would cache the wrong port from the session override.

## Layer 2: Path Watcher — auto-model-switch.py

### Systemd Units

**`/etc/systemd/system/hermes-config.service`:**
```ini
[Unit]
Description=Hermes Agent: handle config.yaml model change
After=network.target
PartOf=hermes-config.path

[Service]
Type=oneshot
ExecStart=/home/<user>/.hermes/scripts/auto-model-switch.py
User=<user>
Group=<user>
Environment=HOME=/home/<user>
StandardOutput=journal
StandardError=journal
```

**`/etc/systemd/system/hermes-config.path`:**
```ini
[Unit]
Description=Watch Hermes config.yaml for model changes

[Path]
PathModified=/home/<user>/.hermes/config.yaml
Unit=hermes-config.service

[Install]
WantedBy=multi-user.target
```

**Setup:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable hermes-config.path
sudo systemctl start hermes-config.path
chmod +x ~/.hermes/scripts/auto-model-switch.py
```

### auto-model-switch.py Logic

```python
def main():
    # Acquire flock lock — exit immediately if another instance is running
    if not acquire_lock(): sys.exit(0)

    config_model = read config.yaml model.default
    config_provider = read config.yaml model.provider

    # OpenRouter validation
    if config_provider == "openrouter":
        if model not in curated list → revert to last good state
        save_good_state()
        sys.exit(0)

    # Look up local model
    target = find in MODELS by config_model
    if not target:
        # Cloud model (DeepSeek, etc.) — save state + exit
        # Do NOT stop local GPU services. The cloud model doesn't
        # use the GPU, and keeping the local model warm means
        # instant switch back.
        save_good_state(config_model, config_provider)
        sys.exit(0)

    # State guard + health check
    if state_file matches config model:
        active = get_active_service()
        if active == target: sys.exit(0)  # healthy
        # Dead or mismatch → fall through to restart

    swap_to(target)  # Stop active, start target, wait for API,
                     # write notification with accurate VRAM,
                     # rollback on failure (rollback writes config)
    if success: save_state()
```

### Lock File (flock)

`auto-model-switch.py` acquires an exclusive flock on `~/.hermes/.model-switch.lock` before doing any work. If another invocation is already running, the second one exits immediately. This prevents races between:

- Rapid `hermes config set` calls from the CLI
- Queued path watcher triggers (systemd queues oneshot services)
- Gateway persistence + path watcher overlap

The lock is released on exit via `finally:` block, so even crashes clean up. Rollback path writes config.yaml while the lock is held, so the re-triggered path watcher finds the lock busy and exits — the rollback completes atomically before the next invocation fires.
- **Instant switch back** — the model stays in VRAM, ready to serve
- **Zero GPU contention** — cloud API calls don't touch the local GPU

The only time local services are stopped is during a **local→local** switch, where the new model needs the port and VRAM freed.

### API Wait Before VRAM Read

`swap_to()` calls `wait_for_model(target['port'], timeout_sec=60)` after starting the service but before reading VRAM and writing the notification. This polls `/v1/models` every 1s until the model responds. VRAM is then read from `nvidia-smi` at the correct time — showing the model's actual VRAM usage (e.g., 8821 MB) instead of pre-load system state (2 MB).

### Pitfalls — Path Watcher

- **`chmod +x` required** — systemd fails with `exit-code 203/EXEC` if not executable
- **`reset-failed` after errors** — run `sudo systemctl reset-failed hermes-config.service` after failures
- **Health-check guard is essential** — always verify the service is actually running, not just that the state file matches
- **Path watcher fires on EVERY write** — `atomic_yaml_write` (used by `save_config`) creates a temp file then renames it, triggering `PathModified`. Previously each `hermes config set` call also triggered the path watcher independently (3+ triggers per switch). The `model-switcher` CLI now writes all three config fields atomically in a single YAML write, so only 1 trigger fires. The state guard + lock file prevent wasteful duplicate swaps.
- **Lock file prevents overlapping invocations** — `auto-model-switch.py` uses `flock()` on `~/.hermes/.model-switch.lock`. If another instance is running (e.g., from a queued path watcher trigger), the second one exits immediately. The current invocation handles whatever config it was given.
- **`swap_to()` must fix base_url** — The caller (gateway `/model` picker) writes config with the wrong base_url (the shared `custom_providers[0].base_url`). `swap_to()` now calls `write_model_yaml()` after a successful swap to fix `model.base_url` to `http://127.0.0.1:{target['port']}/v1`. Without this, the #1 cause of "api failed after 3 retries". This is safe because the flock lock prevents re-entry loops.

  The same fix applies to the "restart dead service" path — when the state guard finds a service dead and restarts it, `write_model_yaml()` also fixes the base_url.
- **`get_active_service()` must skip preserved models in both scripts** — If a CPU-only background service (e.g. SmolLM3 with `"preserve": True`) is always running, `get_active_service()` returns it first and never reaches the GPU model to stop it. The preserve-skip guard must be in `get_active_service()` itself, not just in `swap()`, because the function returns at the FIRST active service it finds. Both `model-switcher` and `auto-model-switch.py` have independent implementations — fix both or the path watcher path still breaks.
- **base_url must be updated** — the #1 cause of "api failed after 3 retries"
- **Exactly one watcher** — do not run both user and system `hermes-config.path` for the same config; duplicate triggers cause duplicate logs and race-prone service swaps
- **`write_model_yaml()` must preserve second `base_url`** — the first `base_url:` is under `model:` and gets replaced; the second is under `custom_providers:` and must be preserved (never `continue`/drop it)
- **Path watcher runs independently from gateway** — the gateway's `/model` picker writes config and returns immediately; the path watcher + auto-switch runs asynchronously via systemd. The user sees "switched" confirmation before the service actually starts.

## Layer 3: Cron Watchdog — model-health-watchdog.py

A Hermes cron job with `no_agent=True` running every 30 minutes. Silent when healthy. On detecting a dead service, it restarts and sends notification.

### Setup

```bash
hermes cron create "Model Health Watchdog" \
  --schedule "every 30m" \
  --script model-health-watchdog.py \
  --no-agent
```

### Behavior

1. Check `.model-switch-notify` first → print + delete (delivers pending notifications)
2. Read `config.yaml` → find expected model → check if its service is active
3. Healthy → silent exit (zero stdout = nothing delivered)
4. Dead → restart via `sudo systemctl start` + print notification

### Pitfalls

- **`no_agent=True`** — script stdout IS the delivery. Empty stdout = nothing delivered.
- **Delivery goes to origin** — the chat where the cron was created.
- **Every cron job creates a Telegram session** — even `no_agent`. Brief typing indicator flash is normal.
- **Script runs from `~/.hermes/scripts/`** — so `from models import MODELS` resolves correctly.
- **`sudo` NOPASSWD required** — script calls `sudo systemctl`.
- **Notification checked BEFORE health check** — ensures switch notifications deliver even when service is healthy.

## Connection Error Diagnosis (Recurring Debug Path)

When the user reports "❌ API failed after 3 retries — Connection error" after a `/model` switch to a local model, follow this systematic debug chain. **Do not skip steps.** The most common root causes are: all GPU services dead after a cloud switch, path watcher not firing, or wrong base_url in session override.

### Step 1: Check current config + running services

```bash
# What model is configured?
grep -A 3 "^model:" ~/.hermes/config.yaml

# Are any GPU llama-server processes running?
ps aux | grep -E "llama-server" | grep -v grep | grep -v 8087   # 8087 is CPU SmolLM3

# Quick port health — which ports respond?
for p in 8081 8082 8083 8084 8086 8087 8088 8089 8090 8091 8092; do
  echo -n "$p "; curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:$p/health 2>/dev/null || echo "dead"
done

# All local service states
sudo systemctl list-units --type=service --all | grep llama
```

**All GPU services dead?** This is the most common culprit. The auto-model-switch.py stopped them when switching to cloud and never restarted. Proceed to Step 4.

### Step 2: Check the switch log and state file

```bash
tail -20 ~/.hermes/logs/model-switch.log
cat ~/.hermes/.last-model-state 2>/dev/null || echo "no state file"
```

- **Log shows "Config changed to local model: X" + "Starting X..."?** → The path watcher fired. If start failed, journalctl will show sudo errors.
- **Log shows nothing after last cloud switch?** → The path watcher hasn't triggered. The user may have used a session-only `/model` without `--global`, or the gateway picker flow failed before writing config.
- **State file shows cloud model (deepseek/gpt-5.5)?** → Service was properly stopped, but no local switch completed after.

### Step 3: Verify path watcher is alive

```bash
systemctl --user is-active hermes-config.path     # should say "active"
systemctl --user is-active hermes-config.service   # should be "inactive" (oneshot, only active briefly)
systemctl --user show hermes-config.path -p PathModified  # confirm path
```

**"inactive" / "dead" / "could not be found"?** The watcher died. Restart:
```bash
sudo systemctl daemon-reload
sudo systemctl enable hermes-config.path
sudo systemctl start hermes-config.path
```

### Step 4: Check sudo NOPASSWD works from systemd context

```bash
sudo -n systemctl is-active llama-server.service    # should return "active" or "inactive", not prompt for password
journalctl --user -u hermes-model-switch.service --no-pager -p err -n 10
```

**NOPASSWD scope issue?** Check the sudoers files cover ALL llama-server service variants:
```bash
sudo grep -r "systemctl" /etc/sudoers.d/ | grep -oP "llama-server[^\s]*" | sort -u
# Missing services (e.g. gemma3-qat, gemma4, qwen14b, qwen8b, qwen4b) 
# need to be added, OR the user should use a catch-all NOPASSWD rule.
```

**If the user has `(ALL:ALL) NOPASSWD: ALL`** (check `sudo -l`), this should not be an issue. But if only specific commands are allowed, new services need new sudoers entries.

### Step 5: Verify models.py matches custom_providers

```bash
# Confirm ALL models in config.yaml have matching entries in models.py
python3 -c "
import yaml, sys
sys.path.insert(0, '/home/rurouni/.hermes/scripts')
from models import MODELS
mdl_names = {m['model_name'] for m in MODELS}
cfg = yaml.safe_load(open('/home/rurouni/.hermes/config.yaml'))
cp = cfg.get('custom_providers', [{}])[0].get('models', {})
for name in cp:
    if name not in mdl_names:
        print(f'MISSING from models.py: {name}')
    else:
        port = [m['port'] for m in MODELS if m['model_name'] == name][0]
        print(f'OK: {name[:40]:40s} → port {port}')
"
```

**Mismatch?** The quant label differs between models.py and custom_providers (e.g., `Q4_K_M` vs `Q4_K_S`). Update models.py to match the actual config names. The gateway's `_local_model_runtime_base_url()` (gateway/run.py:13671) and `auto-model-switch.py` both match on `model_name`.

**Extended: Three-Layer Desync Check** — models.py vs custom_providers is only ONE of three layers. Also verify against the **actual service files**:

```bash
# Verify ALL THREE layers agree on model file names
echo "=== Service Files (what loads) ==="
for f in /etc/systemd/system/llama-server*.service; do
  name=$(basename "$f")
  gguf=$(grep -oP '/models/llm/[^/]+/\K[^\s]+\.gguf' "$f" 2>/dev/null || echo "(none)")
  port=$(grep -oP -- '--port \K\d+' "$f" 2>/dev/null || echo "?")
  echo "$name: $gguf (port $port)"
done

echo ""
echo "=== models.py (what scripts match on) ==="
python3 -c "
import sys; sys.path.insert(0, '/home/rurouni/.hermes/scripts')
from models import MODELS
for m in MODELS:
    print(f'{m[\"service\"]:40s} → {m[\"model_name\"]}')
"

echo ""
echo "=== custom_providers (what picker shows) ==="
python3 -c "
import yaml
cfg = yaml.safe_load(open('/home/rurouni/.hermes/config.yaml'))
for name in cfg.get('custom_providers', [{}])[0].get('models', {}):
    print(f'  {name}')
"
```

**Each model_name needs to match across ALL THREE layers.** If the service file loads `qwen3-14b-Q4_K_M.gguf` but custom_providers lists `Qwen3-14B-UD-Q4_K_XL.gguf`:

1. The `/model` picker writes the wrong name (`Qwen3-14B-UD-Q4_K_XL.gguf`) to config
2. `auto-model-switch.py`'s `is_cloud_model()` checks `models.py` → doesn't find it → classifies as **cloud** → does nothing
3. No service is started, base_url stays at the shared custom_providers port → **Connection error**

**Fix the desync:** Update `custom_providers` model names to match what the service files actually load (which is also what `models.py` should reference). Then `auto-model-switch.py` can correctly find and manage the service.

### Step 6: Verify gateway port resolution (the previous fix)

```bash
# Test what base_url the gateway would resolve for a given model
cd /home/rurouni/.hermes/hermes-agent
python3 -c "
import importlib.util as u
from pathlib import Path
h = Path('/home/rurouni/.hermes')
p = h / 'scripts' / 'models.py'
spec = u.spec_from_file_location('_m', str(p))
m = u.module_from_spec(spec)
spec.loader.exec_module(m)
MODELS = m.MODELS

# Test a local model name
name = 'gemma-3-12b-it-qat-Q4_0.gguf'
for md in MODELS:
    if md['model_name'] == name:
        print(f'Gateway resolves: http://127.0.0.1:{md[\"port\"]}/v1')
        break
else:
    print(f'{name} -> NOT FOUND in models.py — gateway falls back to custom_providers[0].base_url!')
"
```

### Step 7: Manual start + config fix (last resort)

If the service is dead and the path watcher is not firing:
```bash
# Start the desired model directly
sudo systemctl start llama-server-gemma3-qat.service   # or whatever service
sleep 8
curl -s --max-time 5 http://127.0.0.1:8091/v1/models | head -5

# Once running, update config to match
python3 -c "
import yaml, tempfile, os
cfg = yaml.safe_load(open('/home/rurouni/.hermes/config.yaml'))
cfg['model'] = {
    'default': 'gemma-3-12b-it-qat-Q4_0.gguf',   # matching GGUF name
    'provider': 'custom:local',
    'base_url': 'http://127.0.0.1:8091/v1',       # matching port
}
fd, tmp = tempfile.mkstemp(prefix='.config.yaml.', suffix='.tmp', dir='/home/rurouni/.hermes', text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as f:
    yaml.safe_dump(cfg, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
    f.flush()
    os.fsync(f.fileno())
os.replace(tmp, '/home/rurouni/.hermes/config.yaml')
"
# Also update the state file so auto-switch doesn't re-trigger
echo -e 'gemma-3-12b-it-qat-Q4_0.gguf\ncustom:local' > ~/.hermes/.last-model-state
```

This bypasses the path watcher entirely and directly sets the system to a known-good state. The user can then use `/model` to switch to other models normally.

### Previous Fix: Gateway Port Resolution

The last time this exact symptom occurred, the root cause was the gateway storing `custom_providers[0].base_url` (the picker grouping key, e.g. port 8090) in the session override instead of the selected model's actual port. The fix at `gateway/run.py:13671` (`_local_model_runtime_base_url`) resolves the per-model port from `models.py` before storing the override:

```python
@staticmethod
def _local_model_runtime_base_url(model_name, provider, base_url):
    # Only applies to custom:local providers with localhost URLs
    # Loads ~/.hermes/scripts/models.py and matches model_name
    # Returns f"http://127.0.0.1:{model['port']}/v1"
```

If this issue recurs, verify the fix is still present in `gateway/run.py` and hasn't been reverted by an update.

## File Inventory

See also `references/local-llama-model-diagnostics.md` for direct API probes, GGUF metadata checks, wrong-port diagnosis, duplicate watcher detection, and the Phi-4 native-context pitfall.

**⚠️ Skill copy may be stale.** The scripts at `~/.hermes/skills/autonomous-ai-agents/hermes-agent/scripts/` are reference copies and may not reflect live versions at `~/.hermes/scripts/`. The live versions are canonical. If the skill copy lacks features (e.g., flock lock, cloud-warm behavior, single atomic model-switcher write), the live copy is the correct one.

| File | Purpose |
|------|---------|
| `~/.hermes/scripts/models.py` | Shared model definitions (name, service, port, gguf filename) |
| `~/.hermes/scripts/quick-start-model.py` | Legacy gateway hook target (current gateway uses path watcher instead) |
| `~/.hermes/scripts/auto-model-switch.py` | Path-watcher trigger: swap services on config change + wait for API + notify |
| `~/.hermes/scripts/model-health-watchdog.py` | Cron watchdog: health check + auto-restart + notification dispatch |
| `~/.hermes/scripts/model-switcher` | User's `models` alias: interactive TUI + direct shortcut CLI |
| `~/.hermes/.last-model-state` | State guard: prevents re-trigger loops |
| `~/.hermes/.model-switch-notify` | One-shot notification carrier |
| `~/.hermes/logs/model-switch.log` | Auto-switch activity log |
| `~/.hermes/logs/model-health.log` | Watchdog activity log |
| `/etc/systemd/system/hermes-config.path` | systemd path watcher (`PathModified on config.yaml`) |
| `/etc/systemd/system/hermes-config.service` | systemd oneshot service (runs `auto-model-switch.py`) |
