fix(presence): hide cli sessions; use numeric mac build

This commit is contained in:
Peter Steinberger
2025-12-12 17:27:11 +00:00
parent c417517f43
commit 7dab927260
5 changed files with 71 additions and 17 deletions

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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}"

View File

@@ -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<Record<string, unknown>>;
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(

View File

@@ -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,