Files
openclaw/docs/concepts/presence.md
Peter Steinberger 295809df98 feat(geolocation): resolve client addresses to a coarse city via a bundled plugin (#128546)
* feat(geolocation): resolve client addresses to a coarse city via a bundled plugin

The Activity identity card could show a client's IP address but not where it
was, so an operator still had to look the address up by hand.

Add a bundled `geolocation` plugin that owns address-to-place resolution behind
one authenticated route, `GET /plugins/geolocation/lookup?ip=`. It downloads a
MaxMind-format database on first lookup into the state directory, answers from
that local copy, and refreshes it monthly, so a lookup never sends an address
to a third party. The Control UI renders the resolved city on the device row
next to the address and the client-reported time zone.

The default source is DB-IP City Lite under CC BY 4.0. That license requires
attribution, so every response carries the credit and the UI renders it next to
the value; the database is downloaded at runtime and never redistributed.
Plugin code and the `maxmind` reader are MIT. No free city-level IP database is
MIT-licensed, so the obligation lives with the data rather than the code, and
`databaseUrl` plus the attribution fields make the source swappable.

No new core provider kind: with one implementation the plugin owns everything
through the existing HTTP-route seam, keeping core plugin-agnostic. A second
provider is what would justify promoting this to a registry contract.

Availability and lookup failure stay distinguishable: a missing or still
downloading database answers 503, never `found: false`. A failed refresh serves
the cached copy, and a body that does not parse as an MMDB is discarded without
replacing a working database.

* fix(docs): correct geolocation config examples and add zh-CN glossary entries

The config examples used `plugins.<id>` instead of the real
`plugins.entries.<id>.config` shape, which the docs config-example
validator and src/config/docs-config-examples.test.ts both reject.
New doc labels also need zh-CN glossary entries.

* chore(labeler): cover the geolocation extension directory

AGENTS.md requires a labeler entry plus a GitHub label for every new
plugin surface; test/scripts/labeler-extension-coverage.test.ts enforces
the labeler half.

* fix(geolocation): address review findings on caching, download bounds, and scope

Cold-start lookups were permanently suppressed. The loader cached one promise
per address including failures, so the 15s browser deadline expiring against a
first download that takes ~46s cached a blank forever, and a mounted row only
looks up again when its IP changes. Lookups now return a discriminated
located/absent/unavailable result: only definitive answers are cached, and the
element retries an unavailable one on a widening 5s/15s/45s schedule.

Download limits ran after allocation. The size check happened only after
`response.arrayBuffer()` had buffered the whole body, and gunzip had no output
ceiling, so a replaced source or a compression bomb could exhaust Gateway memory
before rejection. The body now streams against a compressed ceiling enforced
per chunk, and inflation uses zlib's maxOutputLength.

Cached placements were not scoped to the Gateway. The cache keyed only by
address while endpoint and credentials come from the shared Gateway context, so
a switch could render the previous Gateway's answer. The shared reset hook now
supports multiple subscribers - a single slot silently dropped whichever
registered first - and the geolocation cache subscribes.

Unresolvable ranges no longer trigger a download. Only loopback suppresses `ip`
at connect, so Tailscale carrier-grade-NAT and LAN addresses are recorded and
displayed. No geolocation database contains them, so a tailnet-only or LAN-only
Gateway was downloading 125 MB to answer nothing. The route now answers those
ranges without loading the database, using the already-public
`isPrivateOrLoopbackHost` seam so the SDK surface budget is unchanged.

The quickstart queried a reserved documentation range while showing a located
response, which cannot happen; it now uses a routable address and documents the
not-found case.

* fix(deps): resync the lockfile after dropping the net-policy dependency

The geolocation plugin briefly depended on @openclaw/net-policy before
switching to the already-public isPrivateOrLoopbackHost SDK seam. The
package.json entry was removed without regenerating the lockfile, so the
frozen-lockfile install failed and every downstream CI job failed with it.
2026-08-24 04:56:03 -07:00

6.1 KiB

summary, read_when, title
summary read_when title
How OpenClaw presence entries are produced, merged, and displayed
Debugging live status on the Control UI Devices page
Investigating duplicate or stale instance rows
Changing gateway WS connect or system-event beacons
Presence

OpenClaw "presence" is a lightweight, best-effort view of:

  • the Gateway itself, and
  • user-visible clients connected to the Gateway (mac app, WebChat, nodes, etc.)

Presence renders live connection metadata in the Control UI Devices page (under Settings → Devices) and the macOS app's Instances tab.

This page covers the Gateway client roster. To detect the Mac you most recently used and route node alerts there, see Active computer presence.

Presence fields (what shows up)

Presence entries are structured objects with fields like:

  • instanceId (optional but strongly recommended): stable client identity (usually connect.client.instanceId)
  • host: human-friendly host name
  • ip: best-effort IP address; the geolocation plugin resolves it to a coarse city where one is available
  • version: client version string
  • deviceFamily / modelIdentifier: hardware hints
  • timeZone: self-reported IANA zone (for example Europe/Vienna); browsers report it during connect, and it stays useful when the connecting IP is loopback, tunneled, or CGNAT
  • mode: ui, webchat, cli, backend, node, probe, test
  • lastInputSeconds: seconds since last user input, if known
  • reason: free-form client-supplied string; the Gateway itself only emits self, connect, and disconnect
  • deviceId, roles, scopes: device identity and role/scope hints from the connect handshake
  • ts: last update timestamp (ms since epoch)

Producers (where presence comes from)

Presence entries are produced by multiple sources and merged.

1) Gateway self entry

The Gateway always seeds a "self" entry at startup so UIs show the gateway host even before any clients connect.

2) WebSocket connect

Every WS client begins with a connect request. On successful handshake the Gateway upserts a presence entry for that connection.

Why ephemeral control-plane connections do not show up

CLI commands, backend RPC clients, and probes often connect briefly. To avoid retaining that churn for the full presence TTL, clients in cli, backend, or probe mode are not turned into presence entries. Test-mode clients stay tracked because test suites use them as stand-ins for real clients.

3) system-event beacons

Clients can send richer periodic beacons via the system-event method. The mac app uses this to report host name, IP, version, and liveness metadata. Physical input activity is not part of this generic beacon; the purpose-specific native node event described in Active computer presence owns it. The Mac tags these beacons with system-presence-clear-last-input; current Gateways use that backward-compatible marker to remove any input recency retained from an older app. The beacon also carries a fixed 30-day value so older Gateways that ignore the tag overwrite exact recency instead of retaining it. No new activity is sampled for this compatibility value.

4) Node connects (role: node)

When a node connects over the Gateway WebSocket with role: node, the Gateway upserts a presence entry for that node (same flow as other WS clients).

Merge + dedupe rules (why instanceId matters)

Presence entries are stored in a single in-memory map, keyed case-insensitively by the first available of, in order: a paired device id, connect.client.instanceId, or the per-connection id as a last resort.

Ephemeral control-plane clients are excluded from tracking entirely (see above), so their connection ids never become keys. For every other client, the connection id fallback means a client that reconnects without a stable instanceId shows up as a duplicate row.

TTL and bounded size

Presence is intentionally ephemeral:

  • TTL: entries older than 5 minutes are pruned
  • Max entries: 200 (oldest dropped first)

This keeps the list fresh and avoids unbounded memory growth.

Remote/tunnel caveat (loopback IPs)

When a client connects over an SSH tunnel / local port forward, the Gateway may see the remote address as 127.0.0.1. To avoid recording that tunnel address as the client's IP, connect handling omits ip entirely for detected-local (loopback) clients rather than writing the loopback address into the entry.

Consumers

Control UI Devices page

The Devices page joins system-presence with durable pairing and node records. It pins the Gateway self beacon first and uses matching device or instance ids for live platform, version, model, and input-recency metadata.

macOS Instances tab

The macOS app renders the output of system-presence and applies a small status indicator (Active/Idle/Stale) based on the age of the last update.

Debugging tips

  • To see the raw list, call system-presence against the Gateway.
  • If you see duplicates:
    • confirm clients send a stable client.instanceId in the handshake
    • confirm periodic beacons use the same instanceId
    • check whether the connection-derived entry is missing instanceId (duplicates are expected)
How physical Mac input selects an active node and routes connection alerts. When typing indicators are sent and how to tune them. Outbound streaming, chunking, and per-channel formatting. Gateway components and the WebSocket protocol that drives presence updates. The wire protocol for `connect`, `system-event`, and `system-presence`.