Symptom

A server that went offline correctly showed the error (WARN) state. After the user deleted that offline server node, it re-appeared on screen every few seconds and the app kept trying (and failing) to reconnect — “as if it were still getting a beacon,” even though the server was offline.

Root cause

The client has a delete-tombstone system (ClientNodeManager.deletedServers) with three guards — update() refuses to re-create a tombstoned id, ClientServerConnector.performConnectionSanityCheck blocks reconnects, and EventClient’s retry loop breaks on it. But ClientBeaconWireHandler undercut all of them: on every incoming beacon it called nodeManager.allowServer(wire.installId), clearing the tombstone, then reconnected. A half-online / flapping server keeps multicasting its cheap UDP beacon even while its HTTPS/SSE endpoint is unreachable, so each ~5s re-probe cleared the tombstone, re-added the node, and spun a doomed reconnect → alarm(WARN) → repeat. The beacon path was the only automatic tombstone-clear, so it was the sole resurrection vector. This contradicted the product rule that a user’s delete is durable and re-adding is a deliberate action.

Fix

In BeaconWireHandler.kt, removed the unconditional nodeManager.allowServer(wire.installId) from the beacon path and added an early return when nodeManager.isServerDeleted(wire.installId) is true. A deleted server is now ignored by beacons entirely; the tombstone is cleared only by an explicit manual re-add (ClientServerConnector line ~111, the TEMP_ID_FOR_CONNECTING path), which is unchanged. Because allowServer is a no-op for ids that were never tombstoned, this changes behavior only for deleted servers.

Prevention

When a piece of state encodes a deliberate user decision (a delete tombstone), no passive/background signal (a beacon, an SSE retry, a poll) should silently reverse it — only an explicit user action should. Added ClientBeaconWireHandlerDeletedServerTest: a beacon for a tombstoned server must neither call allowServer nor connectWire, while a beacon for a live server still connects. The test injects the installId seam and uses virtual time, so it is environment-independent.