Problem found!
The issue has to do with the fact that we are really doing two syncs on connect:
1. When the server first announces it supports LED states
2. When we get the first focus when the window is created
If these two happen quickly enough then there will not be time for the server to push an update of its state between the two syncs. And since we sync by toggling, not by setting, we end up toggling twice because we haven't realised the server has already been updated (the client just isn't informed yet).
There are two ways of solving this, with their own drawbacks:
a) Avoid the first sync. It is only really needed if the server announces support when we already have focus, so we could limit it to that case. That would solve this case, but it would not solve a similar case where focus is quickly toggled back and forth without the server having time to respond. That will probably not happen because of user interaction, but it might be a side effect of e.g. a window quickly popping up and going away.
b) Keep track of when we are already trying to sync. The problem is we don't really know if the sync is done as the server might ignore some states (e.g. ScrollLock in most cases). So we might block future, legitimate syncs of other keys. Fences could be an option, but that is complex and not supported by all servers.
I think a) might be good enough, even if it leaves a theoretical corner case.