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.

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, currentUrlexpectedUrlEnvironment 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>
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 18365there'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.