Recursos
2Instal·la
npx skillscat add russ534/myopenclawbot/hermes-gateway-platform-diagnostics Instal·la des del registre de SkillsCat.
Hermes Gateway Platform Diagnostics
When to Use
Trigger on any of:
- "微信端没反应 / 微信端咋回事 / 微信断了" / "I sent X from WeChat and got no reply"
- "TG 端没反应 / Telegram 挂了" (and TG was previously working)
- "Discord/Slack/Signal 端没动静"
- "Hermes 那个 X bot 怎么不回了"
- "Platform X went silent N days ago" — any silence narrative where the user knows the platform used to work
- "X 模型用不了 / 切到 X 不行 / 切了 model 用不了" after a
/modelswitch — seereferences/providers-yaml.md§ "切到模型 X 用不了" (provider/model failure class, distinct from silent-platform)
Do NOT use this skill when:
- The whole gateway is dead (every platform silent + no heartbeat) →
hermes-runtime-self-config - A platform failed at cold-boot (log shows
No adapter available for <platform>) →hermes-runtime-self-config(Telegram section) - The user is asking how a platform works in general (read
gateway/platforms/<name>.pyinstead)
When the user says "微信端咋回事" / "TG 端没反应了" / "Hermes 那个 Discord bot 怎么不回了了" / "X 平台断了", and the gateway process itself is alive, this is the playbook.
Distinct failure classes (don't conflate them)
| Class | Symptom | Likely cause | Where covered |
|---|---|---|---|
| Gateway itself dead | Every platform silent | OOM, crash loop, plugin load fail at boot | hermes-runtime-self-config (s6 restart, exit-diag) |
| Cold-boot plugin-load failure | No adapter available for <platform> in log, gateway runs with one fewer platform |
Plugin import timeout, proxy misconfig (Telegram), DNS race | hermes-runtime-self-config (Telegram section) |
| Runtime silent | Gateway alive, adapters loaded, ONE platform went quiet on a specific date | Upstream token expiry, account banned/deleted, DM-policy silent block, transport-level hang | This file |
| Connected but the turn is stuck | gateway_state.json says connected, the adapter is polling, yet the user gets no reply and the log repeats session busy elsewhere |
The session's turn is blocked inside a tool call — usually an unanswered tool-approval prompt (~300 s timeout) or a hung subprocess; every new inbound re-queues behind it | Turn stuck / queue backlog section below |
| OS-resource exhaustion | Gateway process alive (CPU often 0%), ALL platforms silent simultaneously, hermes gateway status hangs, even simple wc -l / find time out, sessions DB not appending new rows even when the user is sending messages |
Container memory + swap exhausted at the host level — hermes coroutines starved by IO wait, look like every platform went silent at once | OS-resource exhaustion section below — requires container-level intervention, not config edits |
| Provider/model failure | Platform is delivering fine; user says "model X doesn't work / 切到 X 用不了" after a /model switch; errors.log shows provider=<name> model=<name> summary=HTTP NNN retry loop |
Upstream key rejected (401), upstream 5xx (relay or upstream-of-relay broken), or model name not in upstream's table (404); not a hermes platform or session issue | references/providers-yaml.md § "切到模型 X 用不了" |
The diagnostic is "when did this platform stop, and what changed at that moment." A sessions-DB query by source answers this in one pass.
Generic procedure (run in order)
Step 1: Last activity per platform (single query)
import sqlite3, datetime
conn = sqlite3.connect('/opt/data/state.db')
cur = conn.cursor()
print("=== platform → last session ===")
for src, mx, cnt in cur.execute(
"SELECT source, MAX(started_at), COUNT(*) FROM sessions GROUP BY source"
):
print(f" {src:15s} count={cnt:3d} last={datetime.datetime.fromtimestamp(mx):%Y-%m-%d %H:%M:%S}")
conn.close()The diagnostic answer: a platform whose last=<days/weeks ago> while other platforms are recent. Time gap without a gateway restart = upstream-side event (token expired, account banned, channel revoked).
For deeper inspection (last N sessions, last N messages per platform):
import sqlite3, datetime
conn = sqlite3.connect('/opt/data/state.db')
cur = conn.cursor()
cur.execute("""
SELECT s.id, s.started_at, COUNT(m.id)
FROM sessions s LEFT JOIN messages m ON m.session_id = s.id
WHERE s.source = ?
GROUP BY s.id ORDER BY s.started_at DESC LIMIT 10
""", ('weixin',)) # ← change to target platform
for sid, st, msg_cnt in cur.fetchall():
print(f" {datetime.datetime.fromtimestamp(st):%Y-%m-%d %H:%M:%S} msgs={msg_cnt:4d} {sid}")Step 2: Read the live in-process log
# The live Python logger (gateway.log) — refreshes within seconds
grep -E "platform=<name>|<PlatformName>|Sending response|inbound message" /opt/data/logs/gateway.log | tail -30Patterns to look for:
[Platform] Sending response (N chars) to <id>— outbound succeeded (if missing, outbound broken)inbound message: platform=<name> user=<id> msg='…'— inbound processed (if missing, inbound broken)weixin: restored N context token(s)— adapter init (each gateway startup)
Three distinct log streams to know:
/opt/data/logs/gateway.log— in-process Python logger, live, low latency/opt/data/logs/gateways/default/current— s6 stdout capture, buffered to 100MB chunks/opt/data/logs/gateway-exit-diag.log—previous_unclean_exitrecords across restarts (last_heartbeat_at tells you when the previous instance actually died)/opt/data/logs/gateway-shutdown-diag.log— pre-shutdown pstree snapshot
If gateway.log shows healthy outbound on the silent platform, the user-side message didn't reach the adapter at all — likely transport or upstream-blocked, not a Hermes bug.
Step 3: Read the adapter's transport type
# Top of adapter file gives the transport in one line
head -1 /opt/hermes/gateway/platforms/<name>.py
grep -nE "Long.poll|long_poll|getupdates|Webhook|WebSocket|_poll_loop" /opt/hermes/gateway/platforms/<name>.py | headTransport → failure-shape map:
- Long-poll (iLink, Telegram) — silent on 401; echoes empty buffers indefinitely. Token expiry looks identical to "no messages today" until you check
saved_atorgetconfig. - Webhook (Slack, Feishu) — upstream-side socket close, then 403/404 on next POST. Logged as a WARNING, usually visible.
- WebSocket (Matrix, Signal-bridge) — silence after timeout; reconnect attempts visible in log.
Step 4: Token / account file age
ls -la /opt/data/<platform>/accounts/ 2>/dev/null
# Then inspect saved_at:
cat /opt/data/<platform>/accounts/*.json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('saved_at'))"Account file age vs gateway uptime tells you:
- Older than ~3 months (iLink) → upstream likely rotated the credential
- Older than ~30 days (Telegram) → token unusually stale
- Same as latest gateway restart → fresh credential, problem is elsewhere
Step 5: .env policy silent block
grep -E "<PLATFORM>_DM_POLICY|<PLATFORM>_ALLOW_ALL_USERS|<PLATFORM>_ALLOWED_USERS" /opt/data/.envThe silent-block pattern:
<PLATFORM>_DM_POLICY=pairing
<PLATFORM>_ALLOW_ALL_USERS=false
<PLATFORM>_ALLOWED_USERS= ← empty = block all non-pairedUser side effect: messages silently dropped, no error feedback. Unlike token issues, this affects ALL users from the perspective of "what reaches the adapter." But — the adapter DOES see them; it's the authz layer that drops them. Look for pairing / authz_mixin log lines on inbound.
Fix per platform:
hermes pairing list/hermes pairing approve <user-id>- Or flip
<PLATFORM>_ALLOW_ALL_USERS=true(only for trusted-DM deployment)
Step 6: Per-platform specifics
Filled references:
references/weixin-ilink.md— iLink Bot token expiry, long-poll echo buffer, re-pair procedure (verified case, 2026-10)
Placeholders (fill in when first encountered):
references/telegram-runtime.md— runtime polling failures distinct from cold-boot plugin loadreferences/webhook-sig.md— webhook signature failures (Slack, Feishu, DingTalk)references/bridge-process.md— WhatsApp/Signal bridge pidfile and port checks
After diagnosis, present the user with:
- The symptom (X days no inbound from platform Y) — one line with the data
- The most likely cause — one sentence with the evidence
- 2-4 numbered options for the fix
Do not ask "is this OK?" — present the options, let them pick.
Self-repair overrides the options rule. When the user says "自我修复 / 自我检修 / 错了的你自己改正 / 你直接修好 / 不用问我" (or any equivalent self-repair directive), do not present options — pick the highest-confidence fix, execute it, and report what you actually did. The "stop and ask" pattern is for ambiguity; "self-repair" is the user explicitly removing the ambiguity. The exception is destructive ops (rm -rf, docker rm, anything that loses data); there, ask once with a single sentence on what you'll do, then act on confirmation. The rule of thumb: a self-repair directive is a delegation of judgment, not a delegation of execution. The agent still picks the right action; the user just doesn't want to be in the loop.
Turn stuck / queue backlog (connected but mute)
When the user says "没连上 / 没反应" but every platform reads connected, the transport is fine — check the turn, not the adapter.
# 1. Platform truth: state + error message + active agent count
python3 -c "import json;d=json.load(open('/opt/data/gateway_state.json'));print(d['gateway_state'],'agents=',d['active_agents']);[print(k,v['state'],v.get('error_message')) for k,v in d['platforms'].items()]"
# 2. Is inbound queuing behind a busy session?
grep -E "session busy elsewhere|re-queued a pending event|Cold boot: dropping" /opt/data/logs/gateway.log | tail
# 3. What was the stuck session's last action?
grep "<session_id>" /opt/data/logs/agent.log | tail -5Read that session's last rows in state.db: an assistant message carrying tool_calls with no matching tool result after it = the turn is blocked in that tool. Two shapes seen in practice:
- Approval prompt pending — the tool wanted something the approval layer gates (sudo bypass,
execute_coderunning shell). The turn sits for the full approval timeout (~300 s), then the tool returnsBLOCKED: … timed out without user response. Every message the user sends in that window is queued, so the platform looks dead. - Hung subprocess / kernel — a
terminal/execute_codecall never returns; no child process under the gateway pid, and no furtherAPI call #Nline for that session.
Fixes, in order:
- Answer or cancel the approval in the chat (
/approve//denyreach the runner even while the session is busy). - Wait out the approval timeout (~5 min) — the turn ends on its own and the queue drains immediately. Verified.
- Do not restart the gateway to unstick a turn. Startup auto-resumes interrupted turns (
Scheduled auto-resume for N restart-interrupted session(s)), which replays the same tool call — if that call was a container-recreate/stop, the restart repeats the damage. - Killing the
execute_codekernel process does not unstick an approval-blocked turn. The block is in the approval layer, not in the kernel: the kernel may still be alive (or freshly spawned) while the turn waits on the button. Watch for theBLOCKED: … timed out without user responseline — the turn then ends on its own and the queue drains within seconds. Verified: a 301 s block ended by itself, no intervention needed.
Also name the cold-boot drop instead of "your messages didn't arrive": with platforms.telegram.extra.drop_pending_on_cold_boot: true everything sent while the adapter was offline is discarded, not queued ([Telegram] Cold boot: dropping Telegram updates queued while offline).
Restart semantics: process ≠ container (and the /proc/PID/environ trap)
s6 supervises the gateway process, not the container. kill -TERM <gateway-pid> (s6 respawns it) or s6-svc -r gives a fresh gateway inside the SAME container, with the SAME docker run -e environment. Only docker rm + docker run changes the container's initial env.
That does not mean a container recreate is needed to change env-derived settings: hermes_cli/env_loader.py::load_hermes_dotenv() loads /opt/data/.env into the process's os.environ at startup (and re-asserts it on hot reload). So .env edits + a process restart cover everything the gateway reads through env — proxy vars included.
/proc/<pid>/environ is the env at exec time only. It never reflects os.environ mutations made after the process started, so a dotenv-provided key (proxy vars, platform tokens, API keys) is absent from it even when the running process has it. Concluding "the gateway still doesn't have NO_PROXY" from /proc is a false negative — check the dotenv file and the consuming code path, or prove it behaviorally with the in-process probe below.
Never docker stop / docker rm the container the agent itself runs in to "pick up env changes": the kernel dies mid-turn, the restart is unsupervised, and startup auto-resume then replays the interrupted turn.
Before kill -TERM <gateway-pid>, check it is not your own ancestor. A CLI/terminal agent is its own process (ppid = the exec/ssh session), so killing the gateway leaves it alive; but a turn running inside the gateway (a platform session, an api_server run, a cron tick) is a child of that pid, and the kill lands on the caller mid-turn. One check settles it:
ps -o pid,ppid,cmd -p <gateway-pid> $$ # proceed only if $$'s ppid != <gateway-pid>When your own ppid IS the gateway, hand the restart to the user or launch it detached (nohup … & / a background process) so the signal cannot reach the caller. After the restart verify three things, not one: gateway_state.json shows every platform connected, gateway.log has no unexpected Rehydrated persisted /model override, and the next turn's agent.log line names the model you expect.
Reset a session's persisted /model override (back to the config default)
A /model switch typed in a platform chat is persisted per session and survives every restart — each boot logs Rehydrated persisted /model override for session=… model=… provider=…. Reverting it means clearing three copies, only one of which is authoritative:
gateway_routing.entry_json.model_override— the real store ({model, provider, base_url}). Set it tonull.sessions.model+sessions.model_config(JSON) — the transcript-side record. Point model/provider/base_url/api_mode at the config default and keep_usage_anchorintact (it carries context accounting; dropping it corrupts token totals).- Restart the gateway process (
kill -TERM <gw-pid>, s6 respawns) so the in-memory routing store re-reads the DB. Editing the DB alone changes nothing: the running gateway keeps the old override in memory and re-persists it on the next session update.
import json, sqlite3
c = sqlite3.connect('/opt/data/state.db')
key = 'agent:main:telegram:dm:<chat_id>'
d = json.loads(c.execute('select entry_json from gateway_routing where session_key=?', (key,)).fetchone()[0])
d['model_override'] = None
c.execute('update gateway_routing set entry_json=? where session_key=?', (json.dumps(d, ensure_ascii=False), key))
c.commit()Verify all three, not one: no Rehydrated persisted /model override line after the restart; the next turn's agent.log reads model=<default>; and a probe turn through /api/sessions/{id}/chat returns runtime.model == <default>.
Do not "fix" it by sending /model again in the chat or by editing config.yaml — config.yaml is only the default, and the session override outranks it.
Prove the gateway's own network path (in-process turn)
A curl from a shell proves the shell's path, not the gateway's — different process, different env, different client. To prove the gateway itself can reach its provider, run a real turn through the gateway's own API server:
python3 /opt/data/skills/devops/hermes-gateway-platform-diagnostics/scripts/prove_gateway_path.py
# → {"status": 200, "content": "在的", "seconds": 6.4}A 200 with real content proves adapter + session machinery + provider reachability from the gateway's env (proxy / NO_PROXY included). A failure returns the gateway's own error text instead of a curl artefact. Use this whenever the claim under investigation is "the gateway can't reach the model / fallback 502s" — it settles the question in one call and does not need a restart.
OS-resource exhaustion (separate failure class)
A gateway can be alive in ps but completely non-functional when the container's host-level memory and swap are exhausted. The signatures look identical to "every platform went silent at the same time" — but the cause is below hermes's config layer.
Smoking-gun checks (run these FIRST when "all platforms silent" + gateway status hangs):
free -m # Look for swap ~100% used + phys free < 5%
ps -p <gateway-pid> -o pid,stat,pcpu,pmem,rss # RSS large, but %CPU 0% = coroutine starved, not working
cat /proc/<gateway-pid>/io | head # write_bytes plateau'd = IO is stuckIf you see swap ~100% used, hermes CPU 0%, AND wc -l / find time out, this is OS exhaustion, not a platform adapter problem. Don't waste turns grepping gateway.log or running the Step 1-6 procedure above — none of those will find the cause.
Causes (in order of frequency):
- hermes internal memory leak — sessions DB grows large, in-process caches grow without bound; symptoms recur within minutes-hours of every restart.
- Container memory limit too low —
docker inspect <container>→HostConfig.Memory< 8GB is fragile for hermes; long sessions with high token counts spike the working set. - Host swap insufficient — without swap, OOM-killer fires and you get the "gateway dead" failure class. With swap but too little, you get THIS failure class (process alive, IO blocked, slowly recovers when host frees memory).
- iLink / Telegram long-poll keep-alives accumulating — older gateway versions held per-platform buffers that grew over time.
Fixes (escalate from least to most disruptive):
- Add swap on the host — does not require hermes restart, gives immediate relief.
fallocate -l 8G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile. The user can do this on the fnOS host. - Bump container memory limit in docker-compose / docker run, then restart the container. Will disconnect the active agent session.
- Bump host swap to 8-16GB — same as (1) but at host level; more durable.
- Investigate hermes memory growth — requires reading hermes source / raising an upstream issue. Long-term fix.
What to tell the user (in 1 sentence, 2-3 options): "Container memory + swap are exhausted — this is below hermes's config layer. Pick: (a) add swap on host, no restart; (b) restart container with more memory, will disconnect us; (c) leave it and wait for the leak to settle (may not recover)."
PITFALLS
Say which layer actually restarted. "Gateway 又起来了" is ambiguous: s6 respawning the gateway process and recreating the container are different events with different effects (process → picks up
.env; container → picks updocker run -e). This user tracks the difference and will call out the blur. Name it: "进程重启,容器没重建" or "容器重建".Re-measure a stale "proxy returns 502 for LAN" claim before any disruptive action. A conclusion carried over from an earlier session (or from the other endpoint's transcript) is a hypothesis, not a fact. One authenticated request through the same proxy settles it — e.g.
ProxyHandler({'http': 'http://<host>:7890'})+ the real bearer key againsthttp://192.168.31.201:8788/v1/modelsreturned200with 23 models while the belief said 502. Never plan a container recreate, env surgery, or NO_PROXY rewrite around an unverified network claim; verify first, then act (or don't).state.dbtimestamps are epoch floats, not ISO strings.WHERE timestamp > '2026-10-19T20'silently returns zero rows (string compare against a float column) — the classic "the table is empty" false alarm. Filter numerically (timestamp > <epoch>) or withdatetime.fromtimestamp()in Python. Same forsessions.started_at/last_activity_at.hermesis not onPATHinside the container shell. Use/opt/hermes/.venv/bin/hermes …(orcd /opt/hermes && .venv/bin/hermes); a barehermes gateway statusfails withcommand not foundand looks like the CLI is missing.session_turn_leasesis the "who owns this turn right now" table (conversation_id, holder, acquired_at, expires_at). A lease older than the session's last message = a turn that never released, i.e. the queue-backlog class above.微信端
/start永远不回,是设计。 gateway.run 在 inbound 阶段直接Ignoring /start platform ping for session ...,不让它走到 reply。这跟 Telegram 不一样,Telegram/start会回欢迎语。用户用/start测微信是否通,会得出"链路断了"的误判——但实际是设计性吞。诊断时要先 grep 这一行,避免跑全套 silent-platform 流程。Long-poll adapters produce zero warnings on silent-failure upstream. A long-poll that's waiting on an expired token echoes the same buffer indefinitely — no 401, no log entry, no metric. Silence IS the symptom for these adapters. Only Step 1 (sessions DB query) and Step 4 (token age) detect this. If both methods are inconclusive, hit the upstream's
getconfig/getMeendpoint directly with the saved token — a 401 there proves it.messagestable is keyed bysession_id, not by platform. To find "last message from weixin," JOIN withsessionsonsource. A naiveSELECT source FROM messages GROUP BY sourcefails withno such column: source— the column lives onsessions, notmessages.hermes gateway statuscan hang indefinitely on a healthy degraded gateway. Useps -ef | grep "hermes gateway run"for the truth — not the status command. The status command's 15s timeout hides real failures. It also answers only "is a gateway process alive" (✓ Gateway is running (supervised by s6 …)) and never lists per-platform state — read/opt/data/gateway_state.json(gateway_state,active_agents,platforms.<name>.state/error_message) when the question is which endpoint is up.Different channels = different sessions rows. When the user says "I told you yesterday on TG," check whether THIS channel's sessions are recent in Step 1 — channels are isolated. A current Telegram failure won't show up in a WeChat session table.
Don't trust "I don't see errors in the log" as "nothing's wrong." A long-poll waiting on an expired token produces no warnings — connection never errors, just returns empty. For long-poll adapters, silence IS the symptom. The logs are useful for webhook/WS failures, not for long-poll.
Re-pair is a USER action, not an agent action. Even with
hermes pairing list/approve, iLink Bot (and similar OAuth platforms) re-pair requires the user to scan a QR code in the messaging app. The agent can present the QR/instructions but cannot complete the scan. State this once, don't retry.Don't conflate cold-boot plugin-load failures with runtime failures. A persistent
[Platform] Sending responselog line followed by NO subsequent log on the same platform means the gateway-side outbound works and upstream is the issue. A log line that ends abruptly (no[Platform] Sending responseline for days) means the gateway-side adapter stopped talking — different failure class, different fix.iLink Bot token refresh has no upstream callback. Tencent rotates personal-bot tokens without warning. There is no
/refreshendpoint and no notification. Manual monitoring bysaved_atage is the only mitigation.Image-source attribution lies — the cache path is not the source platform. When the user sends an image AND says "this is what WeChat shows" (or any other platform), the image is delivered via whichever channel the user is currently typing on (Telegram in most cases), not the platform being depicted. The
cache/images/img_XXXXXXXX.jpgpath is the AGENT-side cache written by the receiving platform, not the platform that produced the screenshot. Don't conclude "the image came from Telegram, so the user wasn't on WeChat" — the user can screenshot WeChat, then upload the screenshot to Telegram. The image CONTENT is the platform being discussed; the image FILE PATH is irrelevant to the source platform. When in doubt, ask the user which platform they were on when the issue happened, not where the image is cached.iLink SDK error "Your request was not processed. Send it again if you still want me to carry it out." is from the iLink client SDK, not from Hermes. The message means the iLink cloud received the user's message but the iLink long-poll echo buffer didn't get an ack from hermes within the SDK's timeout. This is the OS-resource-exhaustion symptom (above) surfacing as a user-visible error string — it does NOT mean hermes rejected the request, and it does NOT mean the user needs to "re-pair". If this appears repeatedly without Hermes-side log entries for the corresponding message, the diagnosis is hermes-coroutine-starvation, not platform-token-expiry.
When the user says "自我检查" / "你又咋了" after a string of
Your request was not processederrors, run Step 1 (sqlite state.db per-platform last activity) and report the data FIRST before forming any hypothesis. The user is reading the visible error stream and assumes the agent itself is broken; the diagnostic answer is usually "platform X last inbound N hours ago, no outbound responses" which immediately identifies the failure class. A 1-line statement of the data + a 1-sentence read on the most likely cause is the response that stops the frustration loop. Do NOT first try to reproduce the error (the iLink SDK timeout window is seconds; by the time you re-test, the condition has cleared and you look like you have no idea what's happening). Do NOT apologize for the platform being broken — read the data, name the class, give 2-3 options.iLink
errcode -14 / "session timeout"is recoverable viahermes gateway restart— do NOT escalate to re-pair on first sight. iLink maintains a server-side session for the long-poll connection that can expire if the adapter's poll cycle stalls. The token is still valid; only the server-side session is gone. A freshhermes gateway restartre-handshakes the long-poll, the server-side session re-handshakes, and inbound resumes with the same token. Verified case (2026-10): thebbd950b849d7@im.bottoken returned -14 to a direct curl, but a gateway restart cleared it within seconds. Only if the -14 persists after a restart should re-pair be considered. (Detail:references/weixin-ilink.md§ "iLink -14 recovery".)A platform can be functional end-to-end while the sessions DB shows no recent rows. The gateway has a persisted in-memory outbound queue that drains on startup. Inbound messages are written to the sessions table only after the response cycle completes. If a platform's last
sessions.started_atis hours/days old but the user just confirmed messages are working, the rows may simply be lagging behind a freshly-restarted adapter that is mid-drain. Cross-check with the livegateway.log[Platform] Sending responselines, not just the sessions DB.Direct-curl probing the upstream is NOT a valid refutation of what the user observed end-to-end. A raw
curlto an upstream service (e.g. iLinkgetconfig/getupdates, or a relay like codebuddy2api's/v1/chat/completions) goes through a different auth path, different handshake state, and different error-wrapping than the in-process adapter. If the user reports "platform X is working" / "I sent a message and got a reply", running a curl that returns a stale-session error or a wrapped HTML 401 in JSONerror.messagedoes NOT prove the platform is broken — it only proves the curl didn't take the same path the adapter does. The correct move on user-confirmed-working is to ACCEPT IT and stop probing; the user is the ground-truth end-to-end observer. Re-verify only with a curl that mimics the adapter's full request envelope (auth header, channel version, app_client_version, etc.), or viahermes -z "ping" --usage-file /tmp/u.jsonto route through hermes itself.User says "X 端断了 / X 端没反应" — load this skill BEFORE forming a hypothesis, and run Step 1 (state.db per-platform last activity) FIRST. The most common false-negative is the agent declaring "the channel doesn't exist in my connector list / docker ps / memory" without first checking
state.db. A long-poll adapter (iLink, Telegram) that lost its token leaves zero on-host footprint — no docker container, no connector entry changes — but the gateway process is alive and the platform'ssessions.started_atis stuck at the token-expiry timestamp. Conversely, an iLink container on the host is not evidence the hermes-side adapter is healthy: it only means the upstream SDK is up. The first command on a "X 端断了" report is the Step 1 sqlite query, notdocker psand notmanage_connections status. If state.db shows the platform's last session is recent and healthy, the problem is upstream of hermes (token, account, network) — not a missing connector."I think I broke it" framing must not be accepted without evidence. When the user says "应该是你改了不该改的东西" / "did you change something", the agent has no continuous memory across sessions — earlier-session writes are invisible. The honest answer is: "today's session made no writes; if an earlier session wrote something, it's in git/audit/state.db, not in my context — point me at the artifact and I'll investigate." Do not apologize for an action you cannot find in the current session's tool log, and do not invent one to seem cooperative. Check today's own tool calls first (
/opt/data/logs/gateway-exit-diag.logfor gateway config,git statusfor tracked files, state.db for sessions written in this window), then report what you find.Missing automation tools (sshpass / pexpect / paramiko / etc.) → switch to "user pastes commands" mode, do not loop. When a hermes container lacks
sshpass/pexpect/paramikoand the agent is not root, a single failed install attempt is enough — do not retry withapt-get, do not trypip install, do not trypython -c "import". The right move is to write a self-contained copy-paste block for the user to run on the host and report back. Past sessions havingssh fnossucceed is not evidence of present connectivity — verify withwhich sshpass; ls ~/.ssh/before attempting.If the user repeats a status statement 2+ times, your "independent verification" loses. A failure shape this skill must guard against: user says "platform X is working now" → agent runs a direct curl to "confirm" → curl returns an error code (a false negative from the previous pitfall) → agent insists the platform is broken → user says "I literally just got a reply" → agent curls again, gets the same false error → user says "are you deaf? 微信好了". Each iteration erodes trust and burns turns. The stopping condition is: the user has stated end-to-end working → accept it. If the user reports the same status three times, the burden of proof flips — agent must produce a hermes-internal probe (
hermes -z "Reply with PONG" --usage-file, ortail -f /opt/data/logs/gateway.logto catch the next inbound's[Platform] Sending responseline) before re-opening the question. The path to that hermes-internal probe is also a one-call check, not a research project; do not pivot into "let me also look at X, Y, Z first.""之前都没这问题" is a recall violation, not a theory test. When the user pushes back with "this never happened before," the agent's correct response is to (a) look at the platform's actual error/success log for the last 24–72 h (
errors.logfiltered by the platform,gateway.logfiltered bySending response), (b) report what's literally there (one-off 5-min window vs recurring pattern), and (c) explicitly retract any speculative cause the agent emitted earlier. Do NOT double down on the speculative cause ("but iLink really does expire after 12 h, that's how it works") — that defends a story the user has just told you is wrong. The right move is: pull the data, then either (i) confirm a one-off and explain why it looked bigger than it was, or (ii) confirm a real regression and ask the user what changed (a setting, a TokenService rotation, a host reboot the user may not have tied to the symptom). The phrase "之前都没这样的问题" is direct testimony that the agent's current model of the failure is incomplete — treat it as a missing data point, not as a challenge to defend.A missing log line is only evidence when the path logs. Agent turns write
[Platform] Sending response (N chars); script-mode cron deliveries (no_agent) and other direct adapter sends do not. So "I grepped gateway.log and saw nothing" refutes nothing about a cron push — check the job record (hermes cron list: last status, delivery error) and ask the user to confirm receipt, and say plainly which of the two you actually verified.
Cross-references
- Cold-boot, plugin-load, and gateway-itself-dead failures:
hermes-runtime-self-config(Telegram channel section + s6 restart recipe). - WeChat-specific re-pair constraints (no inline buttons, typed path works):
hermes-runtime-self-config"Channel-specific notes" +references/weixin-ilink.mdhere. - Bridge pidfile / port ownership:
references/bridge-process.md. providers:map inconfig.yamltriggering slowhermes -zstartup:references/providers-yaml.md.
Scripts
scripts/probe_silent_platforms.py— one-shot tool: prints per-platform last-session timestamp + token age + .env policy block, for a fast "which platform is dead and why" pass.scripts/prove_gateway_path.py— runs one real turn through the gateway's own api_server (127.0.0.1:8642) and prints{status, content, seconds}; the behavioural proof that the gateway's provider/proxy path works.