Skip to content

Debugging & Troubleshooting (Relay connections and logs) ​

For phone notifications and recordings to arrive, the path is phone app → hosted Relay → local daemon → written to disk. The "connect to Relay" step is initiated by the daemon at startup: the daemon connects to wss://…/message/messages/ws/plugin using the account-level api-key, then hands req frames forwarded by Relay to the Go Dispatcher to route to the daemon's gateway; HTTP-style frames are looped back to the daemon's own HTTP server. In multi-key mode, each api-key label gets its own Relay tunnel.

So the troubleshooting order is always the same three steps: does the CredentialSet have a usable key → is the daemon running → is the tunnel connected.

Three steps to confirm Relay is connected ​

bash
yc auth status           # 1. is an api-key / apiKeys[] in place
yc auth list-api-keys    # with multiple keys, check mode, defaultLabel, label list
yc daemon status         # 2. is the daemon running (also check the relay/tunnels fields)
yc tunnel status         # 3. tunnel connection status

tunnel status looks like this when connected:

json
{ "ok": true, "mode": "relay", "credentialMode": "file-multi",
  "defaultLabel": "phone-a", "connected": true, "env": "production",
  "relayUrl": "wss://openclaw-service.yoooclaw.com/message/messages/ws/plugin",
  "enabled": true, "reconnectAttempt": 0,
  "stale": false,
  "currentUrl": "wss://openclaw-service.yoooclaw.com/message/messages/ws/plugin",
  "expectedUrl": "wss://openclaw-service.yoooclaw.com/message/messages/ws/plugin",
  "tunnels": [
    { "label": "phone-a", "default": true, "connected": true, "reconnectAttempt": 0 },
    { "label": "phone-b", "default": false, "connected": true, "reconnectAttempt": 0 }
  ] }

How to read the fields:

FieldMeaning
moderelay = going through the hosted tunnel; standalone-http = no tunnel — read note for which of the four causes applies (table below)
credentialModeenv-single / file-multi / keychain-single / legacy-file-single / none
defaultLabelThe default api-key label; used by ASR fallback and legacy single-key status fields
envCurrent environment name (development / test / production)
connectedWhether a WebSocket to Relay is established and heartbeating
reconnectAttemptCumulative reconnect count; 0 means it connected once and never dropped
lastDisconnectReasonThe reason for the most recent disconnect (e.g. error: Unexpected server response: 403)
staleThe address the tunnel actually dials (currentUrl) differs from the one the config resolves to (expectedUrl) — usually means you just switched environments and it's reconnecting. Not a failure; wait one reconnect cycle
currentUrl / expectedUrlWhat the default tunnel actually dials / what the current config resolves to
tunnels[]The list of tunnels under multiple keys; each item has label, default, connected, reconnectAttempt
noteAn actionable hint when not connected

mode: standalone-http has four causes — read note before concluding anything:

noteMeaningWhat to do
Relay 未启用,走直连 HTTP (Relay disabled, using direct HTTP)relay.enabled=falseyc config set relay.enabled true, then restart
Relay 已启用但当前 CredentialSet 没有可用 api-key (Relay enabled but no usable api-key)No api-key setAdd an api-key, then yc daemon reload
ingress=proxied:隧道由宿主代理,daemon 仅收 ingestExpected behaviorThe tunnel belongs to the host application; don't debug it here
ingress=direct:隧道关闭,仅接受直接 POSTExpected behaviorDirect POST only

Only when you're deliberately using the direct-HTTP fallback should you go check firewalls, reverse proxies, and the address configured on the phone.

yc tunnel status --client phone-a shows just the specified label. Relay is connected when the daemon starts: if auth status shows daemon.running: false, tunnel status won't show a connection — run yc daemon start first.

Environment / host mismatch ​

The tunnel, light effects, light rules, and ASR all share one cloud host. Precedence is environment variables > cloud.host > relay.url > built-in default:

PrioritySourceNotes
1PHONE_NOTIFICATIONS_ENV / OPENCLAW_HOST_*Only counts when explicitly set; for temporarily flipping environments
2config.cloud.hostEmpty means follow the environment variables
3The domain in config.relay.urlOnly when it is a built-in environment's default domain; ignored when it points at a self-hosted tunnel or proxy (that machine may not serve the plugin-side API)
4Built-in defaultproduction

The cloud block in daemon status reports both the result and its source:

json
{ "cloud": { "env": "test", "host": "…", "source": "cloud.host",
             "lightApiUrl": "https://…", "lightRuleApi": "https://…" } }

source is env / cloud.host / relay.url / default, which makes the usually most time-consuming question — "the config says A, why is it hitting B" — answerable at a glance.

bash
yc config show --format json                                # inspect the actual cloud.host / relay.url
yc config set cloud.host openclaw-service.yoooclaw.com
yc config unset cloud.host                                  # clear it to follow PHONE_NOTIFICATIONS_ENV
yc daemon reload

Tunnel is connected but light effects / transcription return 401

This is the most common mismatch: tunnel status is green, yet light send / lightrule list return 401 Invalid plugin API Key. Api-keys are issued per environment, so the cause is almost always that the phone account and the local api-key live in different environments — not an expired key. Compare cloud.env from daemon status with the environment the phone app is on, align them via cloud.host or PHONE_NOTIFICATIONS_ENV, then run yc daemon reload. On such a 401 the CLI appends the current environment, the masked key, and the next action to the error.

Reading logs ​

The daemon writes file logs to ~/.yoooclaw/profiles/<profile>/daemon.log, rotated daily to daemon.log.YYYY-MM-DD. Two ways to view them:

bash
# search (🟢 pure disk read, no daemon needed)
yc log relay                       # filter by keyword
yc log --level warn --limit 100    # by level / count
yc log --from 2026-05-21           # by date range
yc log +errors                     # error-level since yesterday

# live follow (🔵 continuous tail)
yc daemon logs -f                  # like tail -f
yc daemon logs -f --level error    # only follow errors

You can also just run tail -f ~/.yoooclaw/profiles/default/daemon.log directly.

For autostart / reload problems (the daemon didn't come back after a reboot, or a changed api-key didn't take effect), collect everything with one command:

bash
yc daemon logs --diagnostics --lines 100

It puts the daemon log, ~/.yoooclaw/logs/daemon-supervisor.log, the lock file and process identity, and autostart state into one result; on Linux it adds this boot's systemd user service state and journal. If the CLI isn't on PATH, run it via the actual install path (e.g. /usr/local/bin/yoooclaw). For what the structured diagnostic events mean, see Command Reference · daemon.

Reading key log lines ​

A normal startup + connect roughly looks like:

Log line (excerpt)Meaning
Port 18365 in use, trying 18366 / Bound to 18366The default port was taken, so it auto-incremented (see "Ports" below)
Relay multi-tunnel started: phone-a,phone-b (mode=file-multi, default=phone-a)The daemon has built a tunnel for each label in apiKeys[]
CredentialSet reload(manual): mode=file-multi, default=phone-b, started=..., stopped=...A manual reload or file watch triggered a credential hot reload
Relay tunnel: started (url=… → in-process gateway dispatch)The RelayDispatcher has taken over inbound frames; subsequent reqs call the daemon runtime directly
Relay tunnel: connecting to wss://… (apiKey=ock-…)The connection is being established; the api-key is masked
Relay tunnel: ✔ connected, heartbeat startedConnected, heartbeat started
→ heartbeat "ping" / ← pong receivedA keepalive heartbeat every 10s; seeing this continuously means the connection is stable
Relay tunnel: relay disconnected (…)Disconnected, then enters exponential-backoff reconnection
[relay-dispatcher] req id=… method=recordings.result.writeThe app / cloud called a gateway method over Relay, and it's been dispatched into the daemon
[recording-status] <id> → transcribingA recording status change happened, and it's been appended to recordings/state/events.jsonl

Common symptoms → fixes ​

SymptomCauseFix
tunnel status shows mode: standalone-http, note says no api-keyNo api-key set, so the tunnel was skippedFor a single key, yc auth set-api-key <ock_…>; for multiple keys, yc auth add-api-key <ock_…> --label <label>, then yc daemon reload
connected: false, lastDisconnectReason contains 403Invalid / expired api-key, or account mismatchUse the correct api-key; with multiple keys, first find the specific label with yc tunnel status --client <label>, then yc daemon reload
All 🟡 commands report YOOOCLAW_DAEMON_NOT_RUNNINGThe daemon isn't runningyc daemon start
mode: standalone-http even though an api-key is setFour possible causes, distinguished by noteSee the standalone-http table above
connected: false, reconnectAttempt keeps climbingNetwork unreachable / Relay unreachableCheck the network; you can temporarily fall back to a direct HTTP connection (see below)
stale: true, currentUrl ≠ expectedUrlEnvironment just switched; the tunnel still dials the old addressWait one reconnect cycle, or force it with yc tunnel reconnect
Tunnel is fine but light send / lightrule list return 401The api-key doesn't match the environmentSee "Environment / host mismatch" above
daemon start returns YOOOCLAW_DAEMON_DISABLED_BY_PLUGINThe storage writer lock is held by the Hermes pluginExpected in plugin mode — the daemon shouldn't run; read-only commands still work. Disable the plugin to return to standalone mode
A recording stays at synced with no transcriptASR isn't configured, or the cloud model-proxy ASR is unavailableRetry after yc recording setup-asr --mode api --language auto --non-interactive; also check yc recording events --id <id>
The daemon didn't come back after a reboot (Linux server, nobody logged in)Only login autostart is registered; linger is offyc daemon autostart enable --boot; check bootEnabled / bootWarning in yc daemon autostart status
yoooclaw: command not found inside an Agent, fine in a terminalThe Agent's non-interactive shell didn't load PATHUse the full path printed by the installer, or add PATH to the Agent runner's startup environment and restart it
daemon reload returns running: false while the system service looks alivedaemon.lock doesn't match the actual process identityCheck process.reason in yc daemon logs --diagnostics; yc doctor --fix if needed
Recording audio download fails with 403 in lastErrorThe OSS signed URL expired and the app sent no ossTaskIdNewer apps that send ossTaskId refresh automatically; older requests need the app to resend the recording result
A recording has a lingering lastErrorA historical failure message wasn't clearedThe daemon auto-clears lastError on reaching a successful terminal state like synced / transcribed; restart the daemon and watch the event stream

Active checks and self-tests ​

bash
yc daemon reload      # re-read apiKeys[] and incrementally refresh multiple Relay tunnels
yc tunnel reconnect   # force disconnect and reconnect (after changing api-key or network recovery)
yc tunnel reconnect --client phone-a
yc tunnel +test       # loopback self-test: the daemon sends itself an echo notification, verifying ingest + auth
yc tunnel +test --client phone-a
yc gateway test       # simulate the phone calling /notifications, verifying connectivity / auth
yc doctor             # environment self-check: Go runtime, directory permissions, keychain, daemon, config

Once a notification arrives, lastIngestAt / ingestCount in yc daemon status change, and yc notification search --client <label> will show the notification written for that label — these two are the final proof that "the whole chain is working."

Once a recording arrives, start by looking at the list and the latest event:

bash
yc recording list
yc recording +latest
yc recording events --since 1h --limit 50

To debug a specific recording:

bash
yc recording status <recording-id>
yc recording events --id <recording-id> --watch

The event file lives at ~/.yoooclaw/profiles/<profile>/recordings/state/events.jsonl. It's append-only, and local queries don't depend on whether the daemon is currently running; --watch first prints matching historical events, then keeps tailing new ones.

Ports ​

The listen address is always the loopback 127.0.0.1, starting at port 18365 — there's no need for, and no longer any, interactive port configuration. If 18365 is taken at startup, the daemon automatically increments +1 (18366, 18367, …), writing the actual port into the log and daemon status. So when troubleshooting, trust the port in daemon status — don't assume it's always 18365.

Fallback when not using Relay ​

When Relay is temporarily unavailable, the daemon's HTTP server is always listening locally, so you can set up your own tunnel: use cloudflared or tailscale serve to reverse-proxy to http://127.0.0.1:<port>, and point the phone at that address plus the gateway token — no dependency on the hosted Relay.