fix(broker): dedup presences by session_id on hello
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

When a client reconnects with the same session_id before the 90s
stale sweeper runs, the old ghost presence stays in the connections
map. Result: duplicate entries in list_peers for the same Claude
Code instance.

Now: handleHello iterates connections for matching (meshId, sessionId),
closes the old WS, deletes from map, marks disconnected in DB.
One session_id = one presence, always.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-16 21:40:25 +01:00
parent 2edbfce7d3
commit 5ddb11b2d5

View File

@@ -120,6 +120,7 @@ interface PeerConn {
meshId: string;
memberId: string;
memberPubkey: string;
sessionId: string;
sessionPubkey: string | null;
displayName: string;
cwd: string;
@@ -1691,6 +1692,19 @@ async function handleHello(
const initialGroups = helloHasGroups
? hello.groups!
: (saved?.groups?.length ? saved.groups : (member.defaultGroups ?? []));
// Session-id dedup: if this session_id already has an active presence,
// disconnect the ghost. Happens when a client reconnects after a
// network blip or broker restart before the 90s stale sweeper runs.
// One Claude Code instance = one session_id = one presence, always.
for (const [oldPid, oldConn] of connections) {
if (oldConn.meshId === hello.meshId && oldConn.sessionId === hello.sessionId) {
log.info("hello dedup", { old_presence: oldPid, session_id: hello.sessionId });
try { oldConn.ws.close(1000, "session_replaced"); } catch { /* already dead */ }
connections.delete(oldPid);
void disconnectPresence(oldPid);
}
}
const presenceId = await connectPresence({
memberId: member.id,
sessionId: hello.sessionId,
@@ -1706,6 +1720,7 @@ async function handleHello(
meshId: hello.meshId,
memberId: member.id,
memberPubkey: hello.pubkey,
sessionId: hello.sessionId,
sessionPubkey: hello.sessionPubkey ?? null,
displayName: effectiveDisplayName,
cwd: hello.cwd,