Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b74ca28d1 | ||
|
|
af9397d672 | ||
|
|
97dc2d772f | ||
|
|
2238777a2f | ||
|
|
6b2ba1bba5 | ||
|
|
a48b947242 | ||
|
|
ba9d29b4b9 | ||
|
|
d6376e5142 | ||
|
|
2ea6b4fc17 | ||
|
|
c6fd8eece3 | ||
|
|
4c715526ef | ||
|
|
308265e7ce | ||
|
|
88e9243ce4 | ||
|
|
13ac189365 | ||
|
|
2851a0cfc8 |
@@ -1,5 +1,14 @@
|
|||||||
# Probe-drift round 2 — per-leg before/after evidence
|
# Probe-drift round 2 — per-leg before/after evidence
|
||||||
|
|
||||||
|
> **Historical record** — 2026-09-28: The lines below that describe tanko as
|
||||||
|
> "DSH (DeepSeek Harness)" only reflect what the check reported when it was
|
||||||
|
> running. Tanko's runtime was later found to be **hybrid (DSH + Hermes)** —
|
||||||
|
> the check had a `/root/` hardcoding bug that made it probe the wrong home
|
||||||
|
> directory and report `wrapper-missing:tanko` for an agent with a working
|
||||||
|
> wrapper. This document records the observed output, not the underlying
|
||||||
|
> truth; see `fix/agent-health-root-hardcoding-20260928` for the correction.
|
||||||
|
|
||||||
|
|
||||||
**Date:** 2026-09-10
|
**Date:** 2026-09-10
|
||||||
**Worktree (absolute execution path):** `/root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts`
|
**Worktree (absolute execution path):** `/root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts`
|
||||||
**Branch:** `fm/probe-drift-round2-20260909`
|
**Branch:** `fm/probe-drift-round2-20260909`
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ Agent (systemd) → LITELLM_API_KEY → LiteLLM (:116/v1) → GPU (llama-server)
|
|||||||
|
|
||||||
## Config Pattern — Mandatory Fields
|
## Config Pattern — Mandatory Fields
|
||||||
|
|
||||||
### For Hermes Agents (Mumuni, Koonimo)
|
### For Hermes Agents (Mumuni, Koonimo, Tanko-hybrid)
|
||||||
|
|
||||||
Every Hermes agent's `/root/.hermes/config.yaml` (or `/home/jerome/.hermes/config.yaml`) MUST have:
|
Every Hermes agent's `/root/.hermes/config.yaml` (or `/home/jerome/.hermes/config.yaml`) MUST have:
|
||||||
(Tanko is excluded — migrated to DSH/DeepSeek Harness on 2026-08-27, no longer uses Hermes config.)
|
(Tanko is hybrid — runs both DSH and Hermes since 2026-08-27, so its Hermes config is also checked.)
|
||||||
|
|
||||||
### 1. Main Model
|
### 1. Main Model
|
||||||
```yaml
|
```yaml
|
||||||
@@ -297,7 +297,7 @@ Run the consolidated health check:
|
|||||||
```bash
|
```bash
|
||||||
python3 /root/scripts/agent-health-check.py
|
python3 /root/scripts/agent-health-check.py
|
||||||
```
|
```
|
||||||
This validates all 4 LiteLLM keys, detects GPU port conflicts (ghost processes),
|
This validates each agent's live LiteLLM key against the gateway, including tanko, which runs HYBRID (DSH + Hermes) since 2026-08-27; detects GPU port conflicts (ghost processes),
|
||||||
verifies gateway liveness, confirms Zulip streaming (`edit_message` present),
|
verifies gateway liveness, confirms Zulip streaming (`edit_message` present),
|
||||||
and counts recent errors. Non-disruptive — never restarts anything.
|
and counts recent errors. Non-disruptive — never restarts anything.
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ connectivity recovery including end-to-end DM validation.
|
|||||||
|
|
||||||
| Param | Type | Required | Default | Description |
|
| Param | Type | Required | Default | Description |
|
||||||
|-------|------|----------|---------|-------------|
|
|-------|------|----------|---------|-------------|
|
||||||
| `target` | string | yes | — | Agent name: `mumuni`, `koby`, or `shumba` (Tanko excluded — on DSH since 2026-08-27, no Hermes plugin) |
|
| `target` | string | yes | — | Agent name: `mumuni`, `koby`, or `shumba` (Tanko excluded — hybrid (DSH + Hermes) since 2026-08-27, no Hermes plugin) |
|
||||||
| `branch` | string | no | `master` | Git branch to pull (overridable for pinning) |
|
| `branch` | string | no | `master` | Git branch to pull (overridable for pinning) |
|
||||||
|
|
||||||
## Maintains
|
## Maintains
|
||||||
@@ -55,7 +55,7 @@ connectivity recovery including end-to-end DM validation.
|
|||||||
|
|
||||||
| Host | CT | Proxmox | IP (direct) | Hermes Home | User |
|
| Host | CT | Proxmox | IP (direct) | Hermes Home | User |
|
||||||
|------|-----|---------|-------------|-------------|------|
|
|------|-----|---------|-------------|-------------|------|
|
||||||
| Tanko | CT112 | minipve | 192.168.68.122 | /home/jerome/.hermes | jerome | *(DSH since 2026-08-27 — historical, plugin retired on this host)* |
|
| Tanko | CT112 | minipve | 192.168.68.122 | /home/jerome/.hermes | jerome | *(hybrid (DSH + Hermes) since 2026-08-27 — historical, plugin retired on this host)* |
|
||||||
| Koby | CT111 | storepve | 192.168.68.129 | /root/.hermes | root |
|
| Koby | CT111 | storepve | 192.168.68.129 | /root/.hermes | root |
|
||||||
| Shumba | — | — | 192.168.68.119 | /home/lucky/.hermes | lucky |
|
| Shumba | — | — | 192.168.68.119 | /home/lucky/.hermes | lucky |
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ cp plugins/platforms/zulip/adapter.py \
|
|||||||
{{hermes_home}}/hermes-agent/plugins/platforms/zulip/
|
{{hermes_home}}/hermes-agent/plugins/platforms/zulip/
|
||||||
|
|
||||||
# Fix ownership (was Tanko-only, runs as jerome user)
|
# Fix ownership (was Tanko-only, runs as jerome user)
|
||||||
# RETIRED 2026-08-27: tanko no longer uses the Hermes Zulip plugin (DSH).
|
# RETIRED 2026-08-27: tanko no longer uses the Hermes Zulip plugin (hybrid: DSH + Hermes).
|
||||||
[ "{{target}}" = "tanko" ] && chown -R jerome:jerome \
|
[ "{{target}}" = "tanko" ] && chown -R jerome:jerome \
|
||||||
{{hermes_home}}/hermes-agent/plugins/platforms/zulip/
|
{{hermes_home}}/hermes-agent/plugins/platforms/zulip/
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ gateway restart, and connection validation.
|
|||||||
|
|
||||||
| Param | Type | Required | Default | Description |
|
| Param | Type | Required | Default | Description |
|
||||||
|-------|------|----------|---------|-------------|
|
|-------|------|----------|---------|-------------|
|
||||||
| `target` | string | yes | — | Agent name: `mumuni`, `koby`, or `shumba` (Tanko excluded — DSH since 2026-08-27) |
|
| `target` | string | yes | — | Agent name: `mumuni`, `koby`, or `shumba` (Tanko excluded — hybrid (DSH + Hermes) since 2026-08-27) |
|
||||||
|
|
||||||
## Maintains
|
## Maintains
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ cp zulip-platform-plugins/plugins/platforms/zulip/adapter.py \
|
|||||||
zulip-platform-plugins/plugins/platforms/zulip/plugin.yaml \
|
zulip-platform-plugins/plugins/platforms/zulip/plugin.yaml \
|
||||||
<HERMES_HOME>/hermes-agent/plugins/platforms/zulip/
|
<HERMES_HOME>/hermes-agent/plugins/platforms/zulip/
|
||||||
|
|
||||||
# Fix ownership (was Tanko-only; RETIRED 2026-08-27 — tanko on DSH, no Hermes plugin)
|
# Fix ownership (was Tanko-only; RETIRED 2026-08-27 — tanko on hybrid (DSH + Hermes), no Hermes plugin)
|
||||||
chown -R jerome:jerome <HERMES_HOME>/hermes-agent/plugins/platforms/zulip/ # Tanko only (historical)
|
chown -R jerome:jerome <HERMES_HOME>/hermes-agent/plugins/platforms/zulip/ # Tanko only (historical)
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
|
|||||||
@@ -682,7 +682,7 @@ ssh root@192.168.68.110 "systemctl restart llama-server"
|
|||||||
| 109 | docker-vm | storepve | .7 | Docker host | ❌ |
|
| 109 | docker-vm | storepve | .7 | Docker host | ❌ |
|
||||||
| 110 | gitea | minipve | **.17** | Git | ❌ |
|
| 110 | gitea | minipve | **.17** | Git | ❌ |
|
||||||
| 111 | tdunna | storepve | .129 | Hermes agent — ⛔ REPORT-ONLY (Theo's box, no GC) | ✅ |
|
| 111 | tdunna | storepve | .129 | Hermes agent — ⛔ REPORT-ONLY (Theo's box, no GC) | ✅ |
|
||||||
| 112 | tanko | minipve | .122 | DSH (DeepSeek Harness) agent | ✅ |
|
| 112 | tanko | minipve | .122 | hybrid (DSH + Hermes) agent | ✅ |
|
||||||
| 113 | baggy | amdpve | .114 | Hermes agent | ✅ |
|
| 113 | baggy | amdpve | .114 | Hermes agent | ✅ |
|
||||||
| 115 | scottdenya | amdpve | .75 | Denya OneCare | ❌ |
|
| 115 | scottdenya | amdpve | .75 | Denya OneCare | ❌ |
|
||||||
| 116 | syslog-api | minipve | .116 | LiteLLM + Grafana | ❌ |
|
| 116 | syslog-api | minipve | .116 | LiteLLM + Grafana | ❌ |
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ PVE_NODES = {
|
|||||||
|
|
||||||
# Agent definitions: ct, host, user, pve_node, vault_key_name
|
# Agent definitions: ct, host, user, pve_node, vault_key_name
|
||||||
AGENTS = {
|
AGENTS = {
|
||||||
"tanko": {"ct": 112, "host": "192.168.68.122", "user": "jerome", "pve": "minipve", "vault_key": "TANKO_LITELLM_API_KEY", "runtime": "dsh"},
|
"tanko": {"ct": 112, "host": "192.168.68.122", "user": "jerome", "pve": "minipve", "vault_key": "TANKO_LITELLM_API_KEY", "runtime": "hybrid"},
|
||||||
# abiba = pi agent (.24) — no vault key; its LiteLLM key is read from its
|
# abiba = pi agent (.24) — no vault key; its LiteLLM key is read from its
|
||||||
# local env file (key_env below), not from the shared vault or .bashrc.
|
# local env file (key_env below), not from the shared vault or .bashrc.
|
||||||
# runtime=pi: abiba has run pi-only since the harness purge. There is no
|
# runtime=pi: abiba has run pi-only since the harness purge. There is no
|
||||||
@@ -152,6 +152,18 @@ def ssh(host, cmd, user="root"):
|
|||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_user_home(user):
|
||||||
|
"""Resolve the home directory for a user.
|
||||||
|
|
||||||
|
For 'root', returns '/root'. For any other user, returns '/home/<user>'.
|
||||||
|
This is used to construct paths that reference a user's home directory
|
||||||
|
(e.g., ~/.local/bin/hermes, ~/.hermes/config.yaml) instead of hardcoding /root/.
|
||||||
|
"""
|
||||||
|
if user == "root":
|
||||||
|
return "/root"
|
||||||
|
else:
|
||||||
|
return f"/home/{user}"
|
||||||
|
|
||||||
def http_get(url, headers=None, timeout=5):
|
def http_get(url, headers=None, timeout=5):
|
||||||
"""Return HTTP status code as string."""
|
"""Return HTTP status code as string."""
|
||||||
try:
|
try:
|
||||||
@@ -382,10 +394,10 @@ def check_agents():
|
|||||||
ct = agent["ct"]
|
ct = agent["ct"]
|
||||||
report_only = agent.get("report_only", False)
|
report_only = agent.get("report_only", False)
|
||||||
|
|
||||||
# Tanko runs on DSH (DeepSeek Harness) since 2026-08-27 — it no longer runs a
|
# Tanko runs hybrid (DSH + Hermes) since 2026-08-27 — it runs both DSH and Hermes gateway.
|
||||||
# Hermes gateway, so skip the Hermes gateway/state/streaming/journal checks.
|
# Hermes gateway, so skip the Hermes gateway/state/streaming/journal checks.
|
||||||
# Non-Hermes runtimes have no gateway to probe. dsh = Tanko since
|
# Non-Hermes runtimes have no gateway to probe. dsh/pi-only skip the check;
|
||||||
# 2026-08-27; pi = abiba since the harness purge (.24 is pi-only).
|
# hybrid runs both DSH and Hermes and is checked normally.
|
||||||
if agent.get("runtime") in ("dsh", "pi"):
|
if agent.get("runtime") in ("dsh", "pi"):
|
||||||
is_dsh = agent.get("runtime") == "dsh"
|
is_dsh = agent.get("runtime") == "dsh"
|
||||||
label = "DSH (DeepSeek Harness)" if is_dsh else "pi-only runtime"
|
label = "DSH (DeepSeek Harness)" if is_dsh else "pi-only runtime"
|
||||||
@@ -506,7 +518,7 @@ def check_ct_liveness():
|
|||||||
def check_config_integrity():
|
def check_config_integrity():
|
||||||
"""Verify agent config.yaml parses as valid YAML."""
|
"""Verify agent config.yaml parses as valid YAML."""
|
||||||
for name, agent in AGENTS.items():
|
for name, agent in AGENTS.items():
|
||||||
# Tanko runs on DSH (DeepSeek Harness) since 2026-08-27 — no Hermes config.yaml.
|
# DSH/pi-only runtimes have no Hermes config.yaml; hybrid has both.
|
||||||
if agent.get("runtime") == "dsh":
|
if agent.get("runtime") == "dsh":
|
||||||
print(f" ⏭️ {name}: DSH — no Hermes config.yaml since 2026-08-27")
|
print(f" ⏭️ {name}: DSH — no Hermes config.yaml since 2026-08-27")
|
||||||
continue
|
continue
|
||||||
@@ -519,11 +531,11 @@ def check_config_integrity():
|
|||||||
print(f" ⬜ {name}: cannot SSH — skip config check")
|
print(f" ⬜ {name}: cannot SSH — skip config check")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
home = get_user_home(user)
|
||||||
|
|
||||||
# Check YAML parses
|
# Check YAML parses
|
||||||
yaml_ok = ssh(host,
|
yaml_ok = ssh(host,
|
||||||
"python3 -c "
|
f"python3 -c \"import yaml; yaml.safe_load(open('{home}/.hermes/config.yaml')); print('OK')\" 2>&1 || echo 'FAIL'",
|
||||||
'"import yaml; yaml.safe_load(open(\'/root/.hermes/config.yaml\')); print(\'OK\')" '
|
|
||||||
"2>&1 || echo 'FAIL'",
|
|
||||||
user=user)
|
user=user)
|
||||||
if not yaml_ok:
|
if not yaml_ok:
|
||||||
print(f" ❌ {name}: SSH UNREACHABLE (config check skipped)")
|
print(f" ❌ {name}: SSH UNREACHABLE (config check skipped)")
|
||||||
@@ -562,7 +574,7 @@ def _infisical_invocation_paths(wrapper_body):
|
|||||||
def check_wrapper_integrity():
|
def check_wrapper_integrity():
|
||||||
"""Verify the hermes CLI wrapper exists and can reach hermes-real."""
|
"""Verify the hermes CLI wrapper exists and can reach hermes-real."""
|
||||||
for name, agent in AGENTS.items():
|
for name, agent in AGENTS.items():
|
||||||
# Tanko runs on DSH (DeepSeek Harness) since 2026-08-27 — no hermes CLI wrapper.
|
# DSH/pi-only runtimes have no hermes CLI wrapper; hybrid has both.
|
||||||
if agent.get("runtime") == "dsh":
|
if agent.get("runtime") == "dsh":
|
||||||
print(f" ⏭️ {name}: DSH — no hermes CLI wrapper since 2026-08-27")
|
print(f" ⏭️ {name}: DSH — no hermes CLI wrapper since 2026-08-27")
|
||||||
continue
|
continue
|
||||||
@@ -576,7 +588,8 @@ def check_wrapper_integrity():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check wrapper exists
|
# Check wrapper exists
|
||||||
wrapper = ssh(host, "ls -la /root/.local/bin/hermes 2>/dev/null", user=user)
|
home = get_user_home(user)
|
||||||
|
wrapper = ssh(host, f"ls -la {home}/.local/bin/hermes 2>/dev/null", user=user)
|
||||||
if not wrapper:
|
if not wrapper:
|
||||||
# Check alternate wrapper locations
|
# Check alternate wrapper locations
|
||||||
wrapper = ssh(host, "which hermes 2>/dev/null; command -v hermes 2>/dev/null", user=user)
|
wrapper = ssh(host, "which hermes 2>/dev/null; command -v hermes 2>/dev/null", user=user)
|
||||||
@@ -599,7 +612,7 @@ def check_wrapper_integrity():
|
|||||||
# a removed path (litellm-api-keys.prose.md documents
|
# a removed path (litellm-api-keys.prose.md documents
|
||||||
# `rm -f /usr/local/bin/infisical`) must neither produce a dangling path
|
# `rm -f /usr/local/bin/infisical`) must neither produce a dangling path
|
||||||
# nor trigger the PATH check — it is not an invocation.
|
# nor trigger the PATH check — it is not an invocation.
|
||||||
wrapper_body = ssh(host, "cat /root/.local/bin/hermes 2>/dev/null", user=user) or ""
|
wrapper_body = ssh(host, f"cat {home}/.local/bin/hermes 2>/dev/null", user=user) or ""
|
||||||
wrapper_code = "\n".join(line.split("#", 1)[0] for line in wrapper_body.splitlines())
|
wrapper_code = "\n".join(line.split("#", 1)[0] for line in wrapper_body.splitlines())
|
||||||
invoked_paths = _infisical_invocation_paths(wrapper_body)
|
invoked_paths = _infisical_invocation_paths(wrapper_body)
|
||||||
if "infisical" in wrapper_code:
|
if "infisical" in wrapper_code:
|
||||||
@@ -633,24 +646,35 @@ def check_wrapper_integrity():
|
|||||||
else:
|
else:
|
||||||
print(f" ℹ️ {name}: wrapper resolves creds without infisical (e.g. ~/.hermes/.env) — OK")
|
print(f" ℹ️ {name}: wrapper resolves creds without infisical (e.g. ~/.hermes/.env) — OK")
|
||||||
|
|
||||||
# Check hermes-real exists
|
# Check that the wrapper's target resolves. The fleet's wrappers do NOT
|
||||||
|
# all use a hermes-real indirection — some exec the venv module directly.
|
||||||
|
# Verify the wrapper actually points to something runnable.
|
||||||
hermes_real = ssh(host,
|
hermes_real = ssh(host,
|
||||||
"ls -la /root/.local/bin/hermes-real 2>/dev/null || echo MISS",
|
f"ls -la {home}/.local/bin/hermes-real 2>/dev/null || echo MISS",
|
||||||
user=user)
|
user=user)
|
||||||
if not hermes_real or hermes_real.strip() == "MISS":
|
if hermes_real and hermes_real.strip() != "MISS":
|
||||||
# Check venv path
|
print(f" ✅ {name}: wrapper shape: hermes-real at {home}/.local/bin/hermes-real")
|
||||||
hermes_real = ssh(host,
|
else:
|
||||||
"ls -la /usr/local/lib/hermes-agent/venv/bin/hermes 2>/dev/null || echo MISS",
|
# Try the venv under home
|
||||||
|
venv_home = ssh(host,
|
||||||
|
f"test -x {home}/.hermes/hermes-agent/venv/bin/python && echo OK || echo MISS",
|
||||||
user=user)
|
user=user)
|
||||||
if not hermes_real or hermes_real.strip() == "MISS":
|
if venv_home and venv_home.strip().splitlines()[-1] == "OK":
|
||||||
print(f" ❌ {name}: hermes-real NOT FOUND (wrapper broken)")
|
print(f" ✅ {name}: wrapper shape: direct venv exec ({home}/.hermes/hermes-agent/venv/bin/python)")
|
||||||
_fail(f"wrapper-no-hermes-real:{name}", name)
|
|
||||||
else:
|
else:
|
||||||
print(f" ✅ {name}: hermes-real at alt path")
|
# Try the system-wide venv
|
||||||
|
venv_sys = ssh(host,
|
||||||
|
"test -x /usr/local/lib/hermes-agent/venv/bin/python && echo OK || echo MISS",
|
||||||
|
user=user)
|
||||||
|
if venv_sys and venv_sys.strip().splitlines()[-1] == "OK":
|
||||||
|
print(f" ✅ {name}: wrapper shape: system venv (/usr/local/lib/hermes-agent/venv/bin/python)")
|
||||||
|
else:
|
||||||
|
print(f" ❌ {name}: wrapper target NOT RESOLVABLE (no hermes-real, no venv)")
|
||||||
|
_fail(f"wrapper-no-hermes-real:{name}", name)
|
||||||
|
|
||||||
# Check the .env file has the key
|
# Check the .env file has the key
|
||||||
env_has_key = ssh(host,
|
env_has_key = ssh(host,
|
||||||
"grep -c 'LITELLM_API_KEY' /root/.hermes/.env 2>/dev/null || echo 0",
|
f"grep -c 'LITELLM_API_KEY' {home}/.hermes/.env 2>/dev/null || echo 0",
|
||||||
user=user)
|
user=user)
|
||||||
if env_has_key and env_has_key.strip() not in ("", "0"):
|
if env_has_key and env_has_key.strip() not in ("", "0"):
|
||||||
print(f" ✅ {name}: wrapper + .env key present")
|
print(f" ✅ {name}: wrapper + .env key present")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# swap-gpu-dense-model.sh — Swap RTX 3090 from qwen3.6-27B-code to SmartCode-Fable-5
|
# swap-gpu-dense-model.sh — Swap RTX 3090 from qwen3.6-27B-code to SmartCode-Fable-5
|
||||||
# Run when download completes: ssh root@192.168.68.8 'bash -s' < this script
|
# Run when download completes: ssh llmuser@192.168.68.8 'sudo bash -s' < this script
|
||||||
#
|
#
|
||||||
# Usage: bash swap-gpu-dense-model.sh
|
# Usage: bash swap-gpu-dense-model.sh
|
||||||
# Requires: new model at /home/llmuser/models/SmartCode-Fable-5-27B-UD-Q4_K_XL.gguf
|
# Requires: new model at /home/llmuser/models/SmartCode-Fable-5-27B-UD-Q4_K_XL.gguf
|
||||||
|
|||||||
Reference in New Issue
Block a user