---
summary: "How Clawdis presence entries are produced, merged, and displayed"
read_when:
  - Debugging the Instances tab
  - Investigating duplicate or stale instance rows
  - Changing gateway WS connect or system-event beacons
---
# Presence

Clawdis “presence” is a lightweight, best-effort view of:
- The **Gateway** itself (one per host), and
- The **clients connected to the Gateway** (mac app, WebChat, CLI, etc.).

Presence is used primarily to render the mac app’s **Instances** tab and to provide quick operator visibility.

## The data model

Presence entries are structured objects with (some) fields:
- `instanceId` (optional but strongly recommended): stable client identity used for dedupe
- `host`: a human-readable name (often the machine name)
- `ip`: best-effort IP address (may be missing or stale)
- `version`: client version string
- `deviceFamily` (optional): hardware family like `iPad`, `iPhone`, `Mac`
- `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6`
- `mode`: e.g. `gateway`, `app`, `webchat`, `cli`
- `lastInputSeconds` (optional): “seconds since last user input” for that client machine
- `reason`: a short marker like `self`, `connect`, `periodic`, `instances-refresh`
- `text`: legacy/debug summary string (kept for backwards compatibility and UI display)
- `ts`: last update timestamp (ms since epoch)

## Producers (where presence comes from)

Presence entries are produced by multiple sources and then **merged**.

### 1) Gateway self entry

The Gateway seeds a “self” entry at startup so UIs always show at least the current gateway host.

Implementation: `src/infra/system-presence.ts` (`initSelfPresence()`).

### 2) WebSocket connect (connection-derived presence)

Every WS client must begin with a `connect` request. On successful handshake, the Gateway upserts a presence entry for that connection.

This is meant to answer: “Which clients are currently connected?”

Implementation: `src/gateway/server.ts` (connect handling uses `connect.params.client.instanceId` when provided; otherwise falls back to `connId`).

#### Why one-off CLI commands do not show up

The CLI connects to the Gateway to execute one-off commands (health/status/send/agent/etc.). These are not “nodes” and would spam the Instances list, so the Gateway does not create presence entries for clients with `client.mode === "cli"`.

### 3) `system-event` beacons (client-reported presence)

Clients can publish richer periodic beacons via the `system-event` method. The mac app uses this to report:
- a human-friendly host name
- its best-known IP address
- `lastInputSeconds`

Implementation:
- Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`.
- mac app beaconing: `apps/macos/Sources/Clawdis/PresenceReporter.swift`.

## Merge + dedupe rules (why `instanceId` matters)

All producers write into a single in-memory presence map.

Key points:
- Entries are **keyed** by a “presence key”. If two producers use the same key, they update the same entry.
- The best key is a stable, opaque `instanceId` that does not change across restarts.
- Keys are treated case-insensitively.

Implementation: `src/infra/system-presence.ts` (`normalizePresenceKey()`).

### mac app identity (stable UUID)

The mac app uses a persisted UUID as `instanceId` so:
- restarts/reconnects do not create duplicates
- renaming the Mac does not create a new “instance”
- debug/release builds can share the same identity

Implementation: `apps/macos/Sources/Clawdis/InstanceIdentity.swift`.

`displayName` (machine name) is used for UI, while `instanceId` is used for dedupe.

## TTL and bounded size (why stale rows disappear)

Presence entries are not permanent:
- TTL: entries older than 5 minutes are pruned
- Max: map is capped at 200 entries (LRU by `ts`)

Implementation: `src/infra/system-presence.ts` (`TTL_MS`, `MAX_ENTRIES`, pruning in `listSystemPresence()`).

## Remote/tunnel caveat (loopback IPs)

When a client connects over an SSH tunnel / local port forward, the Gateway may see the remote address as loopback (`127.0.0.1`).

To avoid degrading an otherwise-correct client beacon IP, the Gateway avoids writing loopback remote addresses into presence entries.

Implementation: `src/gateway/server.ts` (`isLoopbackAddress()`).

## Consumers (who reads presence)

### macOS Instances tab

The mac app’s Instances tab renders the result of `system-presence`.

Implementation:
- View: `apps/macos/Sources/Clawdis/InstancesSettings.swift`
- Store: `apps/macos/Sources/Clawdis/InstancesStore.swift`

The store refreshes periodically and also applies `presence` WS events.

## Debugging tips

- To see the raw list, call `system-presence` against the gateway.
- If you see duplicates:
  - confirm clients send a stable `instanceId` in the handshake (`connect.params.client.instanceId`)
  - confirm beaconing uses the same `instanceId`
  - check whether the connection-derived entry is missing `instanceId` (then it will be keyed by `connId` and duplicates are expected on reconnect)
