From 7dab9272605eec35739357a0ccf5de174f95418b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Dec 2025 17:27:11 +0000 Subject: [PATCH] fix(presence): hide cli sessions; use numeric mac build --- .../Sources/Clawdis/PresenceReporter.swift | 5 ++- docs/presence.md | 5 ++- scripts/package-mac-app.sh | 3 +- src/gateway/server.test.ts | 42 +++++++++++++++++++ src/gateway/server.ts | 33 ++++++++------- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/apps/macos/Sources/Clawdis/PresenceReporter.swift b/apps/macos/Sources/Clawdis/PresenceReporter.swift index ec502903c..dfe8d5efa 100644 --- a/apps/macos/Sources/Clawdis/PresenceReporter.swift +++ b/apps/macos/Sources/Clawdis/PresenceReporter.swift @@ -70,7 +70,10 @@ final class PresenceReporter { private static func appVersionString() -> String { let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev" if let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { - return "\(version) (\(build))" + let trimmed = build.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty, trimmed != version { + return "\(version) (\(trimmed))" + } } return version } diff --git a/docs/presence.md b/docs/presence.md index cb03a3e5a..33c380d83 100644 --- a/docs/presence.md +++ b/docs/presence.md @@ -44,6 +44,10 @@ This is meant to answer: “Which clients are currently connected?” Implementation: `src/gateway/server.ts` (WS `hello` handling uses `hello.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: @@ -112,4 +116,3 @@ The store refreshes periodically and also applies `presence` WS events. - confirm clients send a stable `instanceId` in `hello` - 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) - diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index 0a0014be6..502dfd8a6 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -12,8 +12,9 @@ BUNDLE_ID="${BUNDLE_ID:-com.steipete.clawdis.debug}" PKG_VERSION="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")" BUILD_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ") GIT_COMMIT=$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") +GIT_BUILD_NUMBER=$(cd "$ROOT_DIR" && git rev-list --count HEAD 2>/dev/null || echo "0") APP_VERSION="${APP_VERSION:-$PKG_VERSION}" -APP_BUILD="${APP_BUILD:-$PKG_VERSION}" +APP_BUILD="${APP_BUILD:-$GIT_BUILD_NUMBER}" BUILD_CONFIG="${BUILD_CONFIG:-debug}" SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}" SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml}" diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 847aa3cdf..422fa9e7b 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import { type AddressInfo, createServer } from "node:net"; import os from "node:os"; @@ -792,6 +793,47 @@ describe("gateway server", () => { await server.close(); }); + test("cli connections are not tracked as instances", async () => { + const { server, ws } = await startServerWithClient(); + const cliId = `cli-${randomUUID()}`; + ws.send( + JSON.stringify({ + type: "hello", + minProtocol: 1, + maxProtocol: 1, + client: { + name: "cli", + version: "dev", + platform: "test", + mode: "cli", + instanceId: cliId, + }, + caps: [], + }), + ); + await onceMessage(ws, (o) => o.type === "hello-ok"); + + const presenceP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "cli-presence", + 4000, + ); + ws.send( + JSON.stringify({ + type: "req", + id: "cli-presence", + method: "system-presence", + }), + ); + + const presenceRes = await presenceP; + const entries = presenceRes.payload as Array>; + expect(entries.some((e) => e.instanceId === cliId)).toBe(false); + + ws.close(); + await server.close(); + }); + test("refuses to start when port already bound", async () => { const { server: blocker, port } = await occupyPort(); await expect(startGatewayServer(port)).rejects.toBeInstanceOf( diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 780c888d9..d2c9c1c62 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -58,6 +58,7 @@ type Client = { socket: WebSocket; hello: Hello; connId: string; + presenceKey?: string; }; const METHODS = [ @@ -545,10 +546,9 @@ export async function startGatewayServer( `webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`, ); } - if (client) { + if (client?.presenceKey) { // mark presence as disconnected - const key = client.hello.client.instanceId || connId; - upsertPresence(key, { + upsertPresence(client.presenceKey, { reason: "disconnect", }); presenceVersion += 1; @@ -639,8 +639,11 @@ export async function startGatewayServer( return; } + const shouldTrackPresence = hello.client.mode !== "cli"; // synthesize presence entry for this connection (client fingerprint) - const presenceKey = hello.client.instanceId || connId; + const presenceKey = shouldTrackPresence + ? hello.client.instanceId || connId + : undefined; logWs("in", "hello", { connId, client: hello.client.name, @@ -655,15 +658,17 @@ export async function startGatewayServer( `webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${describeHello(hello)}`, ); } - upsertPresence(presenceKey, { - host: hello.client.name || os.hostname(), - ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr, - version: hello.client.version, - mode: hello.client.mode, - instanceId: hello.client.instanceId, - reason: "connect", - }); - presenceVersion += 1; + if (presenceKey) { + upsertPresence(presenceKey, { + host: hello.client.name || os.hostname(), + ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr, + version: hello.client.version, + mode: hello.client.mode, + instanceId: hello.client.instanceId, + reason: "connect", + }); + presenceVersion += 1; + } const snapshot = buildSnapshot(); if (healthCache) { snapshot.health = healthCache; @@ -692,7 +697,7 @@ export async function startGatewayServer( clearTimeout(handshakeTimer); // Add the client only after the hello response is ready so no tick/presence // events reach it before the handshake completes. - client = { socket, hello, connId }; + client = { socket, hello, connId, presenceKey }; logWs("out", "hello-ok", { connId, methods: METHODS.length,