| name | hermes-health-audit |
| description | Full-stack Hermes Agent health audit: observability config, compression tuning, prompt/skill consistency, skill invocation diagnosis, system overload detection, session pattern analysis, tool-use/delegation tuning, plus a full annotated config (model/queue/IO/security), per-channel Feishu output discipline, a prompt-engineering method, and a reverse-QA regression loop. Use when skills stop triggering, model ignores loaded skill steps, context grows unexpectedly, sessions hit compression too often, long tasks get interrupted, the task queue misbehaves, or after editing any prompt/skill/config file. |
Hermes Health Audit
Target: Hermes Agent (https://github.com/NousResearch/hermes-agent)
This skill audits and optimizes Hermes Agent's configuration, behavior, and performance. All paths, config fields, and DB schema refer to Hermes Agent only — not Claude Code, OpenClaw, or any other agent framework. Source code location: ~/.hermes/hermes-agent/ after installation, or clone from GitHub.
Execution Requirements:
- This skill should be executed by Hermes Agent itself (or by an agent that understands Hermes directory structure)
- All
~/.hermes/ paths point to Hermes Agent's data directory
- SQLite queries target
~/.hermes/state.db (Hermes session store)
- Config changes only affect
~/.hermes/config.yaml (Hermes config file)
- Never modify Claude Code (
~/.claude/), OpenClaw (~/.openclaw/), or other agent configs
Backend: AWS Bedrock Claude (us.anthropic.claude-opus-4-8), 1M context window (requires explicit context_length: 1000000 in config — Bedrock static table defaults to 200K, beta header context-1m-2025-08-07 is sent but Hermes resolver needs the override), free unlimited. All optimization targets maximum quality, cost ignored.
6-phase diagnostic covering observability, compression, consistency, invocation, overload detection, and session behavior analysis.
When to Use
- Skills stop triggering after extended conversation
- Model "laziness" — loads skill but ignores steps
- Context window grows unexpectedly or compression fires too early
- Long tasks keep getting interrupted by user messages
- After editing SOUL.md, skills, or config.yaml
- Periodic health check (monthly recommended)
- Onboarding a new teammate to Hermes
- 其他 agent (Claude Code / OpenClaw) 需要对 Hermes 做优化时,作为 reference spec 使用
Quick Start
Run all 6 phases. Each is independent — skip recently verified ones.
Phase 1: Observability Config
Ensure diagnostic signals are visible before debugging anything else.
Required Config (~/.hermes/config.yaml)
model:
default: us.anthropic.claude-opus-4-8
provider: amazon-bedrock
context_length: 200000
display:
tool_progress_command: true
runtime_footer:
enabled: true
fields: [model, context_pct, cwd]
agent:
busy_input_mode: "steer"
飞书/Lark 进度可见性:「只看到 ⏳ Working,没有任何步骤」(实战踩坑)
症状:飞书里长任务只显示一个干巴巴的「⏳ Working — N min」计时器,底下 agent 调什么工具、跑到哪一步全看不见,用户无法判断是在干活还是卡死。
根因:display.platforms.feishu 默认把进度类开关全关了(per-channel 覆盖优先于全局 tool_progress_command)。这是取舍不是 bug——关掉是为了少打扰,但代价是长任务完全黑盒。
display:
platforms:
feishu:
tool_progress: true
interim_assistant_messages: false
busy_ack_detail: true
streaming: false
long_running_notifications: true
判据:用户抱怨「不知道在干嘛 / 是不是卡了」→ 开 tool_progress。怕吵 → interim_assistant_messages 保持 false。这三个开关独立可调,按「要看见 vs 怕打扰」分别取舍。
Choosing busy_input_mode (check session patterns first):
sqlite3 ~/.hermes/state.db "
SELECT
count(*) as total_sessions,
round(avg(message_count)) as avg_msgs,
round(avg(tool_call_count)) as avg_tools,
round(avg(cast(tool_call_count as real)/(message_count/2.0)), 2) as avg_tools_per_turn
FROM sessions WHERE source='feishu' AND message_count > 10;"
| User Pattern | Recommendation | Reason |
|---|
| Sends follow-ups/corrections while agent works | steer | Message injected into current turn, agent adjusts mid-execution |
| Heavy automation, rarely interrupts | queue | Turn completes safely, messages processed after |
| Interactive chat, short tasks only | interrupt | Immediate response to new topic, no long tasks at risk |
| tools_per_turn > 0.8 + frequent corrections | steer | High automation + mid-course corrections = steer sweet spot |
| tools_per_turn > 0.8 + rarely sends mid-task | queue | Let long chains finish undisturbed |
Verify
grep -A8 "runtime_footer:" ~/.hermes/config.yaml
grep "tool_progress_command:" ~/.hermes/config.yaml
grep "busy_input_mode:" ~/.hermes/config.yaml
VALID_FIELDS="model context_pct cwd"
CONFIGURED=$(grep -A5 "runtime_footer:" ~/.hermes/config.yaml | grep "fields:" | grep -oE '\[.*\]')
for field in $(echo "$CONFIGURED" | tr -d '[],' ); do
if ! echo "$VALID_FIELDS" | grep -qw "$field"; then
echo "⚠️ Invalid runtime_footer field: '$field' (silently ignored)"
fi
done
grep "gateway_auto_continue_freshness" ~/.hermes/config.yaml
grep -A8 "tool_loop_guardrails:" ~/.hermes/config.yaml
Key Agent Settings (often missing from default config)
agent:
max_turns: 120
gateway_auto_continue_freshness: 3600
tool_loop_guardrails:
warnings_enabled: true
hard_stop_enabled: true
warn_after:
exact_failure: 2
same_tool_failure: 3
idempotent_no_progress: 2
hard_stop_after:
exact_failure: 5
same_tool_failure: 8
idempotent_no_progress: 5
Gateway vs CLI
- Verbose 通过 CLI flag:
hermes chat -v 或 hermes gateway run -v(不是 config.yaml 字段)
- Gateway hardcodes
verbose_logging=False in gateway/run.py
runtime_footer works in both modes
查看完整 Prompt / Context(Debug 必备)
方法 1:HERMES_DUMP_REQUESTS=1 — 每次 API 调用写完整 request 到文件
HERMES_DUMP_REQUESTS=1 hermes gateway run
输出位置:~/.hermes/sessions/request_dump_<session>_<timestamp>.json
文件内容结构:
{
"timestamp": "...",
"session_id": "...",
"reason": "preflight | non_retryable_client_error | max_retries_exhausted",
"request": {
"method": "POST",
"url": "...",
"body": {
"model": "us.anthropic.claude-opus-4-8",
"system": "(完整 system prompt:SOUL.md + memory + skills index)",
"messages": [{"role":"user","content":"..."}, ...],
"tools": [...],
"max_tokens": 128000,
"thinking": {...}
}
}
}
方法 2:HERMES_DUMP_REQUEST_STDOUT=1 — 直接打到终端
HERMES_DUMP_REQUEST_STDOUT=1 hermes chat 2>&1 | tee /tmp/hermes_dump.json
适合一次性调试,输出巨大建议 pipe 到文件。
方法 3:事后分析已有 dump
ls -lt ~/.hermes/sessions/request_dump_*.json | head -5
python3 -c "
import json, sys, glob, os
files = sorted(glob.glob(os.path.expanduser('~/.hermes/sessions/request_dump_*.json')))
if not files: sys.exit('No dumps found')
d = json.load(open(files[-1]))
body = json.loads(d['request']['body']) if isinstance(d['request']['body'], str) else d['request']['body']
sys_len = len(str(body.get('system', '')))
msgs = body.get('messages', [])
msg_chars = sum(len(str(m.get('content',''))) for m in msgs)
tools_chars = len(json.dumps(body.get('tools', [])))
print(f'System prompt: {sys_len:,} chars (~{sys_len//4:,} tokens)')
print(f'Messages ({len(msgs)}): {msg_chars:,} chars (~{msg_chars//4:,} tokens)')
print(f'Tools definition: {tools_chars:,} chars (~{tools_chars//4:,} tokens)')
print(f'Total payload: ~{(sys_len+msg_chars+tools_chars)//4:,} tokens')
print(f'Model: {body.get(\"model\")}')
print(f'Max tokens: {body.get(\"max_tokens\")}')
"
注意事项:
runtime_footer 的 fields 仅支持 model、context_pct、cwd 三个值,其他字段 silently ignored
context_pct 显示当前 token 占 context window 的百分比(实时)
- 要看绝对 token 数必须用 dump request 方式
- Dump 文件会累积,定期清理:
rm ~/.hermes/sessions/request_dump_*.json
Phase 2: Compression Tuning
Compression = #1 cause of "skill stops working after chatting".
Root Cause Chain
skill_view() 在对话中被模型调用时,内容作为 tool result message 进入 history(可被压缩)
- 预加载 (
-s flag) 的 skill 走 system prompt(永不压缩)
/skill-name 斜杠命令作为 user message 注入
- Context hits threshold → compression summarizes old messages
protect_last_n determines how many recent messages survive
- Skill loaded 30+ messages ago → summarized → steps lost
- Model can't see instructions → "laziness"
⚠️ 实战教训:threshold 看的是「绝对 token 触发点」,不是「窗口百分比」
最容易踩的坑:window 越大就把 threshold 设越高 → 压缩永远不触发 → 每轮重算几十万 token,越来越慢。
真实事故(生产实测):context_length: 1000000 + threshold: 0.75 = 要堆到 75 万 token 才压缩。但一个深挖型任务峰值也就 35-40 万 token,永远够不到触发线,于是 50+ 次 API call 每一轮都背着 35 万 token 全量重算,单轮延迟从 70s 一路涨到 220s,一个指令跑了 34 分钟。
两个独立的代价,都被「晚压缩」放大:
- 慢:input token 越大,单轮推理越慢(实测 in=12k→3s,in=40万→50s+),且每轮重算不可缓存的尾部。
- 注意力涣散:模型在 30 万+ token 里抓不住早先的指令/skill 步骤,表现成「偷懒」「忘了规则」。
正确心智模型:threshold × context_length = 绝对触发 token 数,这个绝对值才是要控的目标。让它落在「模型注意力还清醒 + 单轮还不卡」的区间(实测 10-18 万 token 比较舒服),而不是窗口的某个固定百分比。
| 你的 context_length | 推荐 threshold | = 绝对触发点 | 说明 |
|---|
| 200K | 0.5 | 10 万 | 触发早、每轮轻快,长任务首选 |
| 1M | 0.15-0.2 | 15-20 万 | 1M 是应急余量,日常别真用满 |
| 1M(错误示范) | 0.75 | 75 万 | ❌ 普通任务永不触发,等于没压缩 |
关键:不要因为「免费无限量」就把 threshold 拉高。免费省的是钱,省不了「大上下文本身慢 + 注意力涣散」这两笔账。
Recommended Config
model:
context_length: 200000
compression:
enabled: true
threshold: 0.5
target_ratio: 0.2
protect_last_n: 40
protect_first_n: 3
hygiene_hard_message_limit: 400
Auxiliary compression model:
auxiliary:
compression:
provider: bedrock
model: us.anthropic.claude-opus-4-8
context_length: 1000000
Tuning Table(按绝对触发 token 调,不是按百分比)
| Parameter | Conservative | Aggressive | Guidance |
|---|
| 绝对触发点 (threshold×window) | 18 万 | 8 万 | 越低=压得越勤、每轮越轻、注意力越聚焦 |
| protect_last_n | 20 | 60 | Higher = skills survive longer |
| protect_first_n | 3 | 5 | Higher = early context preserved |
免费档(Bedrock/employee)也要主动压缩:省钱 ≠ 该用满窗口。绝对触发点压到 10-18 万、protect_last_n≥40,既保 skill 存活又保每轮轻快。
Verify
grep -A6 "^compression:" ~/.hermes/config.yaml
sqlite3 ~/.hermes/state.db "SELECT id, input_tokens, cache_read_tokens, message_count FROM sessions ORDER BY started_at DESC LIMIT 1;"
Phase 3: Prompt Consistency Audit
3.1 System Prompt Inspection
sqlite3 ~/.hermes/state.db "SELECT system_prompt FROM sessions ORDER BY started_at DESC LIMIT 1;" > /tmp/current_system_prompt.txt
chars=$(wc -c < /tmp/current_system_prompt.txt)
echo "≈ $((chars / 4)) tokens ($(( chars * 100 / 4000000 ))% of 1M)"
3.2 Conflict Detection Matrix
| Conflict Type | Detection | Impact |
|---|
| Skill A vs Skill B | grep contradicting ALWAYS/NEVER on same topic | Random compliance |
| Skill vs SOUL.md | Compare style directives | Output oscillation |
| Skill internal | Steps that undo each other | Partial execution |
| Stale references | Skill mentions deleted skill | Wasted tool calls |
| Instruction saturation | 5+ loaded skills with imperatives | Attention dilution |
3.3 Automated Checks
grep -rn "ALWAYS\|NEVER\|MUST\|禁止\|必须" ~/.hermes/skills/*/SKILL.md | sort -t: -k3 | head -40
grep -rn "skill_view\|skill:" ~/.hermes/skills/*/SKILL.md | \
grep -oE "skill_view\(name='[^']+'" | sed "s/skill_view(name='//;s/'//" | \
sort -u > /tmp/referenced_skills.txt
ls ~/.hermes/skills/ > /tmp/existing_skills.txt
comm -23 <(sort /tmp/referenced_skills.txt) <(sort /tmp/existing_skills.txt)
for d in ~/.hermes/skills/*/SKILL.md; do
desc=$(grep '^description:' "$d" | head -1 | sed 's/^description: *//;s/^"//;s/"$//')
if [ ${#desc} -lt 50 ] && [ ${#desc} -gt 0 ]; then
echo "SHORT(${#desc}): $(basename $(dirname $d))"
fi
done
for d in ~/.hermes/skills/*/SKILL.md; do
lines=$(wc -l < "$d")
if [ $lines -gt 300 ]; then
echo "LARGE($lines): $(basename $(dirname $d))"
fi
done
grep -n "结论先行\|一句话\|简洁\|展开\|详细\|step-by-step" ~/.hermes/SOUL.md 2>/dev/null
grep -rn "输出格式\|output format\|详细\|step.by.step\|完整报告" ~/.hermes/skills/*/SKILL.md
3.4 Naming Quality
Per Anthropic best practices:
- Name: gerund form preferred, no "helper/util/tool/misc"
- Description: third person, WHAT + WHEN, 50-200 chars
- Pushy triggers: description should slightly over-claim
for d in ~/.hermes/skills/*/; do
name=$(basename "$d")
echo "$name" | grep -qiE "^(helper|util|tool|misc|common)$" && echo "VAGUE: $name"
[ ${#name} -gt 40 ] && echo "TOO LONG(${#name}): $name"
done
Phase 4: Skill Invocation Diagnosis
4.1 Verify Skill in Available List
grep "skill-name-here" /tmp/current_system_prompt.txt
Missing → malformed directory or bad YAML frontmatter.
4.2 Session JSON Load Events
cat $(ls -t ~/.hermes/sessions/session_*.json | head -1) | \
python3 -c "import json,sys; d=json.load(sys.stdin); [print(m.get('content','')[:100]) for m in d.get('messages',[]) if 'skill_view' in str(m)]"
4.3 Failure Modes
| Symptom | Root Cause | Fix |
|---|
| Never triggers | Description too narrow | Rewrite with explicit trigger phrases |
| Loads but steps skipped | >500 lines | Split into SKILL.md + references/ |
| Works initially, fails later | Compression ate it | Increase protect_last_n or re-invoke |
| Wrong skill triggers | Overlapping descriptions | Add negative differentiation |
| Partially followed | Internal contradictions | Audit MUST/NEVER conflicts |
4.4 Re-injection Pattern
Add to SOUL.md for critical skills:
When executing a task relying on a previously-loaded skill and >20 messages
have passed since skill_view(), re-invoke skill_view() to refresh content.
Phase 5: Overload Detection
Detect when the system is carrying too much cognitive load.
5.1 Skill Overload
skill_count=$(ls -d ~/.hermes/skills/*/ 2>/dev/null | wc -l)
echo "Skills on disk: $skill_count"
total_lines=$(cat ~/.hermes/skills/*/SKILL.md 2>/dev/null | wc -l)
echo "Total skill lines: $total_lines"
avail_chars=$(grep -A9999 'available_skills' /tmp/current_system_prompt.txt | wc -c)
echo "available_skills section: $avail_chars chars ≈ $((avail_chars/4)) tokens"
Thresholds:
| Metric | Healthy | Warning | Critical |
|---|
| Skills on disk | <40 | 40-70 | >70 |
| Total SKILL.md lines | <8000 | 8000-15000 | >15000 |
| available_skills in prompt | <5000 chars | 5000-15000 | >15000 |
| System prompt total | <20K chars | 20-30K | >30K |
5.2 System Prompt Overload
sys_chars=$(sqlite3 ~/.hermes/state.db "SELECT length(system_prompt) FROM sessions ORDER BY started_at DESC LIMIT 1;")
echo "System prompt: $sys_chars chars ≈ $((sys_chars/4)) tokens"
sqlite3 ~/.hermes/state.db "SELECT system_prompt FROM sessions ORDER BY started_at DESC LIMIT 1;" | \
awk '
/^══.*MEMORY/{section="memory"; next}
/^══.*USER PROFILE/{section="user"; next}
/^<available_skills>/{section="skills"; next}
/^<\/available_skills>/{section=""; next}
{lens[section]+=length($0)+1}
END{for(s in lens) if(s!="") printf "%s: %d chars\n", s, lens[s]}
'
Impact of overload:
-
30K system prompt → model attention spreads thin across instructions
-
70 skills in available_skills → description matching accuracy degrades
- Multiple MUST/NEVER directives competing → model picks randomly
5.3 SOUL.md Complexity
soul_lines=$(grep -c "" ~/.hermes/SOUL.md 2>/dev/null || echo 0)
soul_directives=$(grep -ciE "MUST|NEVER|ALWAYS|禁止|必须|铁律" ~/.hermes/SOUL.md 2>/dev/null || echo 0)
echo "SOUL.md: $soul_lines lines, $soul_directives hard directives"
Thresholds:
| Metric | Healthy | Warning | Critical |
|---|
| SOUL.md lines | <150 | 150-300 | >300 |
| Hard directives | <20 | 20-40 | >40 |
| Directive density | <10% | 10-20% | >20% |
High directive density = model gets "instruction fatigue" — starts ignoring rules.
5.4 AGENTS.md / SOUL.md 指令压缩方法论(实测有效)
当 hard directives 逼近 critical(>40)或在 warning 区(20-40)想主动降载时,不要删信息,要合并表述。实战验证的「总纲 + 明细」压缩法:
grep -ciE "❌|\*\*禁止\*\*|绝对禁止|永远禁止|MUST|NEVER" ~/.hermes/AGENTS.md
压缩手法(以 AWS 网络红线为例,实测 37→15 条):
- 同一主题下散落的 N 条
❌禁止 → 抽成 1 条总纲(如"公网入口只走 CloudFront+ALB(internal)+HTTPS,违反即停")+ 按子类的明细列表
- 明细从「每条独立 ❌ 项」改为「按资源类型分组的一行描述」(SG / LB / EKS / S3 / Lambda / DB / HTTP 各一行)
- 关键技术值零丢失铁律:prefix list ID、IP 基线、bucket 名、四件套字段等具体值必须原样保留,只压表述不压数据
- 压缩前后用 grep 计数验证 directive 数下降、用 diff 确认无技术值丢失
为什么有效:模型注意力按 token 竞争,11 条平级 ❌ 各自抢注意力且易被随机忽略;1 条强总纲 + 明细列表既保留可执行性,又把"必停"的强约束集中到一处,命中率更高。
何时不该压:每条 directive 都是独立事故沉淀且尚未内化时(如刚踩过的坑),保留原样直到形成肌肉记忆;压缩前确认哪些已内化。
5.5 别在 SOUL.md 里手写 skill 清单(反模式)
常见诱惑:在身份提示里列一份"我有哪些 skill"。不要。 框架已经在 system prompt 里自动注入一段 available_skills 索引(全部 skill 的 name + description),SOUL 再抄一遍会撞三个问题:① 重复 → 稀释信号、占 token、拖慢每次调用(正是 5.1/5.2 在量的过载);② 必然过时 → skill 是动态的,手写清单漏更一次就开始撒谎,自动索引永远跟磁盘同步;③ 职责错位 → SOUL 管"我是谁、我的底线",skill 清单是"我手上有什么工具"。
grep -c "available_skills\|## Skills" /tmp/current_system_prompt.txt
该留在 SOUL 的是场景指针而非清单:「写改提示词前先翻 prompt-engineering」「深度调研走 agentcore-deepsearch」这种"在 X 场景该翻哪个 skill"的强提示,是 SOUL 该管的做事原则;泛泛的"我有 a/b/c/d 个 skill"列表则删掉。
5.6 审计脚本自查:grep 的假阴性会骗你
这份 skill 大量用 grep 做存在性判断,有个高频陷阱:在 zsh 里写 grep -cE "A\|B"(双引号内的 \|)转义会失效,命中数返回 0,让你误判"本地没有 / 规则缺失",进而拿假阴性当依据去改配置。判断"某条规则在不在"这种关键存在性检查时,用单独关键词逐个 grep 复核,别只信一次带 \| 的合并查询:
grep -cE "大白话\|破折号" SOUL.md
for kw in 大白话 破折号 先实证; do
printf "%s: %s\n" "$kw" "$(grep -c "$kw" SOUL.md)"
done
通则:审计结论若依赖"grep 返回 0 = 不存在",先换个写法复核一次再下结论。
Phase 6: Session Pattern Analysis
Analyze usage history to identify structural issues and recommend subagent delegation.
6.1 Session Statistics
sqlite3 ~/.hermes/state.db "
SELECT
count(*) as total_sessions,
avg(message_count) as avg_messages,
avg(tool_call_count) as avg_tools,
max(message_count) as max_messages
FROM sessions WHERE source='feishu' AND message_count > 5;"
sqlite3 ~/.hermes/state.db "
SELECT end_reason, count(*) as cnt, avg(message_count) as avg_msgs, avg(tool_call_count) as avg_tools
FROM sessions WHERE source='feishu' AND message_count > 5
GROUP BY end_reason ORDER BY cnt DESC;"
sqlite3 ~/.hermes/state.db "
SELECT count(*) as compression_sessions,
(SELECT count(*) FROM sessions WHERE source='feishu' AND message_count > 5) as total
FROM sessions WHERE source='feishu' AND end_reason='compression';"
6.2 Tool Density Analysis
sqlite3 ~/.hermes/state.db "
SELECT id, message_count, tool_call_count,
round(cast(tool_call_count as real)/message_count, 2) as density,
round(cast(tool_call_count as real)/(message_count/2.0), 2) as tools_per_turn
FROM sessions WHERE source='feishu' AND message_count > 20
ORDER BY tools_per_turn DESC LIMIT 10;"
sqlite3 ~/.hermes/state.db "
SELECT
CASE
WHEN cast(tool_call_count as real)/message_count < 0.3 THEN 'very_low (<0.3) = frequent interrupts'
WHEN cast(tool_call_count as real)/message_count < 0.5 THEN 'normal (0.3-0.5) = standard usage'
WHEN cast(tool_call_count as real)/message_count >= 0.5 THEN 'high (>0.5) = deep automation'
END as category,
count(*) as sessions,
round(avg(message_count)) as avg_msgs
FROM sessions WHERE source='feishu' AND message_count > 10
GROUP BY category ORDER BY sessions DESC;"
Interpretation (corrected for dual-count):
tools_per_turn ≈ 0.85-1.0 → every agent response uses a tool (normal for power users)
tools_per_turn < 0.5 → many turns without tools = user sending messages faster than agent can execute
density 0.4-0.5 is the healthy baseline (NOT 0.8 — that would mean 1.6 tools per turn)
6.3 Interruption Pattern Detection
User sends message while agent executes multi-tool chain → current turn killed.
sqlite3 ~/.hermes/state.db "
SELECT
min(message_count) as earliest,
round(avg(message_count)) as avg_point,
max(message_count) as latest
FROM sessions WHERE source='feishu' AND end_reason='compression';"
sqlite3 ~/.hermes/state.db "
SELECT id, message_count, tool_call_count,
round(cast(tool_call_count as real)/(message_count/2.0), 2) as tools_per_turn
FROM sessions WHERE source='feishu' AND message_count > 30
AND cast(tool_call_count as real)/message_count < 0.3
ORDER BY message_count DESC LIMIT 10;"
ls -t ~/.hermes/sessions/session_*.json | head -20 | while read f; do
if grep -ql "delegate_task" "$f" 2>/dev/null; then
echo "HAS_DELEGATE: $(basename $f)"
fi
done
Benchmark (from real data, 100+ production feishu sessions):
| Metric | Actual Value | Meaning |
|---|
| Avg session length | 72 messages | ~36 turns |
| Tool density | 0.43-0.50 (= 0.85-1.0 tools/turn) | High automation |
| Compression rate | 39% of sessions | Expected for power users |
| Sessions with delegate_task | ~4 recent | Adoption just starting |
⚠️ 早期版本曾推荐「晚压缩」(threshold 0.75 / 延后到 ~140 messages),后被生产事故推翻——见 Phase 2 实战教训。延后压缩看似省调用,实则让每轮重算几十万 token、延迟飙到 220s。现行结论相反:让绝对触发点落在 10-18 万 token,压得勤、每轮轻、skill 也靠 protect_last_n≥40 存活。
Validated config (绝对触发点 10-18 万、protect_last_n=40、max_turns=120):
- 单轮 input 控制在十几万 token 量级,延迟稳定在数十秒内(实测 in=12k→3s,in=40万→50s+)
- 最近 40 条消息内加载的 skill 能熬过压缩
- 长链任务不再在第 60 turn 被硬停
- 上线后复查:实际 compression 触发点、单轮 latency 分布、cache hit
6.4 Cache Hit Ratio (Prompt Caching Health)
sqlite3 ~/.hermes/state.db "
SELECT
round(sum(cache_read_tokens) * 100.0 / (sum(input_tokens) + sum(cache_read_tokens)), 1) as cache_hit_pct,
sum(input_tokens) as total_input,
sum(cache_read_tokens) as total_cache_read
FROM sessions WHERE source='feishu' AND message_count > 5;"
sqlite3 ~/.hermes/state.db "
SELECT id,
round(cache_read_tokens * 100.0 / nullif(input_tokens + cache_read_tokens, 0), 1) as cache_pct,
input_tokens, cache_read_tokens
FROM sessions WHERE source='feishu' AND message_count > 10
ORDER BY started_at DESC LIMIT 10;"
Interpretation:
- Cache hit >95% → excellent (system prompt + early messages fully cached)
- Cache hit 80-95% → normal (some cache misses on long sessions)
- Cache hit <80% → investigate (config changes invalidating cache, or model switching mid-session)
- Observed baseline: >99% cache hit (system prompt dominates repeated calls)
6.5 Long Task & Interrupt Strategy
busy_input_mode: "steer" — 新消息注入当前 turn 作为方向调整(需新 session 生效,见文末生效说明)
gateway_auto_continue_freshness: 3600 — 被中断后 1h 内自动恢复上下文
- Recurring 长任务 → cronjob(独立 session,完全隔离)
- 一次性长任务 → delegate_task(子 agent 边跑边写文件,即使被打断已写入内容不丢)
6.6 Session Reset:跨天「失忆」的元凶(实战踩坑)
症状:用户上午聊一件事,晚上接着说「改成 X」,agent 完全关联不上,像换了个人。
根因:session_reset.mode 默认 both,含一条每天 at_hour(默认 4 点)强制清空整个对话上下文的规则。跨过那个点,上下文被整体 wipe(重置前只把要点存进长期记忆,但对话来龙去脉断了)。这跟「压缩」完全不同——压缩是缩成摘要、要点还在;重置是清空、啥都不剩。
session_reset:
mode: idle
idle_minutes: 1440
at_hour: 4
| mode | 行为 | 适合 |
|---|
both | 每日定时清空 + 闲置清空 | ❌ 跨天连续任务会失忆 |
idle | 只在闲置 N 分钟后清空 | ✅ 跨天 continue 同一任务不断(推荐,token 靠压缩控) |
none | 永不自动清空 | 需手动 /new,最连贯但要自律 |
判据:用户常跨天 continue 同一件事 → 用 idle;任务都是独立短问答 → both 也行。改完靠压缩(Phase 2)控 token,别靠重置。
6.7 多图/多消息聚合:飞书「只收到部分图」(实战踩坑)
症状:一次发 9 张图,agent 说只收到 5 张,漏处理。
根因:飞书多图是 N 个独立 webhook 陆续到达,gateway 用一个聚合窗口把它们并成一条消息。窗口太短(默认 HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS=0.8)→ 9 张被切成「5+2+2」三批,当成三条独立消息,agent 处理完第一批就以为结束了。
grep MEDIA_BATCH_DELAY ~/.hermes/.env
grep "Flushing media batch" ~/.hermes/logs/gateway.log | tail
修复:.env 设 HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS=3.0(3 秒窗口让同组图聚齐)。配套提醒用户:多图一次性选齐发出,别一张张隔很久发。
Phase 7: Skill Usage Analysis & Pruning
Unused skills waste tokens in available_skills index (system prompt overhead).
7.1 Skill Load Frequency
python3 -c "
import json, glob, os, re
from collections import Counter
skill_loads = Counter()
files = sorted(glob.glob(os.path.expanduser('~/.hermes/sessions/session_*.json')), key=os.path.getmtime, reverse=True)[:100]
for f in files:
try:
d = json.load(open(f))
content = json.dumps(d.get('messages', []))
# Match skill_view tool_use blocks
for m in re.finditer(r'skill_view.*?\"name\":\s*\"([^\"]+)\"', content):
name = m.group(1)
if name not in ('skill_view', 'skills_list', 'skill_manage'):
skill_loads[name] += 1
except: pass
all_skills = set(os.path.basename(d.rstrip('/')) for d in glob.glob(os.path.expanduser('~/.hermes/skills/*/')))
never = sorted(all_skills - set(skill_loads.keys()))
print('=== Top loaded skills ===')
for s, c in skill_loads.most_common(20):
print(f' {c:3d}x {s}')
print(f'\n=== Never loaded ({len(never)}/{len(all_skills)} local skills) ===')
for s in never:
path = os.path.expanduser(f'~/.hermes/skills/{s}/SKILL.md')
lines = len(open(path).readlines()) if os.path.exists(path) else 0
print(f' {s} ({lines} lines)')
"
7.2 Pruning Criteria
| Condition | Action |
|---|
| Never loaded in 100+ sessions AND >100 lines | 🔴 Strong candidate for removal |
| Never loaded but <50 lines | 🟡 Low overhead, keep unless >70 total skills |
| Loaded 1-2x in 100 sessions | 🟡 Review — may be niche but legitimate |
| Loaded 5+ times | 🟢 Active, keep |
7.3 Removal Decision
After running 7.1, output a pruning recommendation:
## Skill Pruning Recommendations
### 🔴 Remove (never used, high overhead)
- skill-name (N lines) — reason
### 🟡 Consider removing (rarely used)
- skill-name (N lines) — last used: session_XXX
### 🟢 Keep (actively used)
- skill-name (Nx in 100 sessions)
Important: Only remove skills from ~/.hermes/skills/ (local). Plugin-provided skills (openclaw-imports, etc.) are managed by the plugin system.
Phase 8: Tool-Use & Delegation Tuning
Four high-impact fixes from a real audit where a Claude-backed agent "felt passive" and sub-tasks were overpriced. Verify against your own setup; don't apply blind.
8.1 Enforce tool use for Claude models
tool_use_enforcement: auto only injects the "You MUST use your tools" steering for a hardcoded model list (gpt/gemini/grok/qwen/... in agent/prompt_builder.py) — Claude/Opus/Sonnet are excluded, so they narrate instead of acting and under-search.
agent:
tool_use_enforcement: true
Verify: restart, give a lookup task, confirm it calls a tool (compare web_search freq via Phase 6).
8.2 Cheaper sub-agent model
delegation.model: '' makes sub-agents inherit the parent (Opus) — grunt work (file scans, batch fetches) pays Opus prices.
delegation:
model: us.anthropic.claude-sonnet-4-6
Main convo stays strong; only sub-tasks drop. Gotcha: a wrong model ID fails silently at delegation time, not config load — so prove it exists with a real aws bedrock-runtime invoke-model call first.
8.3 Add deep-research depth
Enforcement (8.1) fixes willingness, not depth — built-in web_search only returns snippets. Mount the agentcore-deepsearch skill (this repo) in mcp_servers (and CC's ~/.mcp.json). Verify: ps aux | grep agentcore-deepsearch/server.py.
8.4 Stale pointers after a prompt refactor
Rules moved (e.g. AGENTS.md → SOUL.md) but memory still points at the old file → silent rule loss. Hermes only reliably loads SOUL.md; AGENTS.md loads only when cwd is the agent home (fails under cron).
grep -rniE "AGENTS\.md|以 .* 为准|回查 .*\.md" ~/.hermes/SOUL.md ~/.hermes/memories/ 2>/dev/null
Repoint stale refs; banner deprecated files with "DO NOT add rules here — moved to SOUL.md".
8.5 Shared browser (CDP) is a busy resource — release it in finally
If the agent reaches closed sources (Reddit/X/etc.) through a local logged-in Chrome over CDP, that Chrome on :9222 is shared by every tool that needs a real login session (deep-search, AI-search report fetchers, image-gen, manual CDP skills). A fetch that opens a tab/WebSocket and doesn't release it on the error path leaves a dangling connection that blocks the next caller.
The bug pattern (seen in production): ws.close() written only on the success path, so a mid-fetch exception (page hang, Runtime.evaluate timeout) skips it and the WebSocket dangles until GC. Fix is to put every release in finally — WebSocket first, then tab close, then any busy-lock clear:
ws = None
tab_id = None
try:
...
finally:
if ws is not None:
try: ws.close()
except Exception: pass
if tab_id:
try: close_tab(tab_id)
except Exception: pass
clear_busy_lock()
Verify the resource is actually clean after a fetch — don't trust "exit 0":
lsof -nP -iTCP:9222 2>/dev/null | grep -c ESTABLISHED
curl -s http://127.0.0.1:9222/json/list | \
python3 -c "import sys,json;print(len([t for t in json.load(sys.stdin) if t.get('type')=='page']))"
8.6 MCP stdio servers need a process-level reload, not just /new
Config changes take effect with a new session (/new), but a code change to an MCP stdio server (the Python/Node process the agent spawns for a tool) does NOT — the running process still holds the old code. stdio MCP servers are spawned per-client and live as long as their parent; to reload, kill the old processes and let the next tool call respawn them with the new code.
ps aux | grep "your-mcp-server.py" | grep -v grep
for pid in <those_pids>; do ppid=$(ps -o ppid= -p $pid|tr -d ' '); echo "$pid <- $(ps -o command= -p $ppid|cut -c1-60)"; done
kill <those_pids>
Then prove the new code loaded by exercising the tool once (e.g. a real fetch), not by checking the process is up. A respawned process with a syntax error will be "running" but broken.
Quick reference config: references/optimized-config.yaml — the Phase 8 settings in one annotated config.yaml, each line noting why.
Phase 9: Config / IO / Prompt Hardening — Defer to pony-agent-blueprint
Phase 8 fixes the four highest-leverage knobs. Phase 9 used to inline the full
config reference, the Feishu/Lark output spec, the prompt-engineering method, and
the reverse-QA loop. All of that now lives in the pony-agent-blueprint skill
— the single source of truth for "what a correct, hardened config looks like."
This audit's job is to detect drift; the blueprint's job is to give the right
answer. Don't duplicate values across both — when the audit finds something
misconfigured, copy the fix from the blueprint.
9.1 What this audit checks (the diagnostic half)
Run these drift checks; for any FAIL, the corrected value is in the blueprint.
hermes config get model.context_length
hermes config get compression.threshold
hermes config get auxiliary.compression.context_length
hermes config get display.busy_input_mode
hermes config get display.platforms.feishu.streaming
hermes config get display.platforms.feishu.tool_progress
hermes config get hooks.feishu-io-patch.enabled
hermes config get session_reset.mode
hermes config get agent.tool_use_enforcement
9.2 Where the "right answer" lives (the prescriptive half → blueprint)
Load the pony-agent-blueprint skill (skill_view(name='pony-agent-blueprint'))
for the prescriptive config. Its files:
| You need… | File in pony-agent-blueprint |
|---|
| Full annotated config, every tunable + WHY | references/pony-config-annotated.yaml |
Feishu/Lark output discipline (config + the feishu-io-patch hook) | references/io-channel-discipline.md + templates/feishu-io-patch/ |
| Message-queue vs task-queue distinction | references/io-channel-discipline.md |
| Prompt-engineering method (rules the model actually obeys) | templates/constraint-prompt-template.md |
| SOUL.md authoring skeleton | templates/SOUL-skeleton.md |
Key non-obvious finding the audit should flag: Feishu output needs TWO
layers — config suppression AND the feishu-io-patch startup hook. Config alone
leaves tables as garbled pipes and images broken. Check both.
9.3 Reverse-QA evaluation (prove the change took effect)
Editing a file ≠ the rule taking effect. After any Phase change, run the
regression loop: restart the gateway → trigger each changed rule with a real,
stateful task → watch for the failure signal → on FAIL, ask the agent "why didn't
you do X, one-sentence root cause, no excuses" and patch the named rule. Use a
stateful session (hermes chat), never a stateless -z probe.
This is the verification gate for the whole audit. A green audit with no
behavioral regression test is an unverified audit.
AGENTS.md reminder (Phase 8.4): Hermes only reliably loads SOUL.md —
AGENTS.md loads only when cwd is the agent home, so it silently fails under
cron/gateway. Put every rule in SOUL.md; tombstone AGENTS.md. (Full rationale
in the blueprint's meta-rule #4.)
Output Format
# Hermes Health Report — [date]
## Phase 1 Observability: ✅/⚠️/❌
- runtime_footer: [enabled/disabled], fields 仅 model/context_pct/cwd
- tool_progress_command: [yes/no]
- busy_input_mode: [steer/queue/interrupt]
## Phase 2 Compression: ✅/⚠️/❌
- 绝对触发点 = context_length × threshold: [value] (recommended: 10-18 万 token;200K×0.5 或 1M×0.15-0.2)
- protect_last_n: [value] (recommended: ≥40)
- current context: [X]% of [Y]K window
## Phase 3 Consistency: ✅/⚠️/❌
- Contradictions found: [N]
- Broken references: [N]
- Oversized skills (>300 lines): [N]
- Vague descriptions: [N]
## Phase 4 Invocation: ✅/⚠️/❌
- Skills in prompt: [N]/[total on disk]
- Naming issues: [N]
## Phase 5 Overload: ✅/⚠️/❌
- Skills count: [N] (threshold: 70)
- System prompt: [N] chars (threshold: 30K)
- SOUL.md directives: [N] (threshold: 40)
- Directive density: [N]% (threshold: 20%)
## Phase 6 Session Patterns: ✅/⚠️/❌
- Avg session length: [N] messages
- Compression rate: [N]% of sessions
- Avg tool density: [N]
- Cache hit ratio: [N]%
- busy_input_mode: steer
## Phase 7 Skill Pruning: ✅/⚠️/❌
- Never-loaded skills: [N]/[total]
- Recommended removals: [N] (saving ~[N] lines from index)
## Actions Taken
- [what was fixed]
## Remaining Issues
- [what needs manual decision]
## ⚠️ 生效方式
修改 config.yaml 后,必须执行 `/new` 开启新 session 才能生效。
当前 session 仍使用旧配置(gateway 在 session 启动时读取 config)。
Scheduling
hermes cron create "0 10 1 * *" \
"Load hermes-health-audit skill, run all 7 phases, output report" \
--skill hermes-health-audit