diff --git a/proto/protocol.xml b/proto/protocol.xml
index bcae78d..2ac7334 100644
--- a/proto/protocol.xml
+++ b/proto/protocol.xml
@@ -1361,12 +1361,33 @@
Notification that the EIS implementation has changed group or modifier
states on this device, but not necessarily in response to an
- ei_keyboard.key event. Future ei_keyboard.key requests must take the
- new group or modifier state into account.
+ ei_keyboard.key event or request. Future ei_keyboard.key requests must
+ take the new group and modifier state into account.
- This event should not be sent in response to ei_keyboard.key events
- that change the group or modifier state according to the keymap. The
- client is expected to track such group or modifier states on its own.
+ This event should be sent any time the modifier state or effective group
+ has changed, whether caused by an ei_keyboard.key event in accordance
+ with the keymap, indirectly due to further handling of an
+ ei_keyboard.key event (e.g., because it triggered a keyboard shortcut
+ that then changed the state), or caused by an unrelated an event (e.g.,
+ input from a different keyboard, or a group change triggered by a layout
+ selection widget).
+
+ For receiver clients, modifiers events will always be properly ordered
+ with received key events, so each key event should be interpreted using
+ the most recently-received modifier state. The server should send this
+ event immediately following the ei_device.frame event for the key press
+ that caused the change. If the state change impacts multiple keyboards,
+ this event should be sent for all of them.
+
+ For sender clients, the modifiers event is not inherently synchronized
+ with key requests, but the client may send an ei_connection.sync request
+ when synchronization is required. When the corresponding
+ ei_callback.done event is received, all key requests sent prior to the
+ sync request are guaranteed to have been processed, and any
+ directly-resulting modifiers events are guaranteed to have been
+ received. Note, however, that it is still possible for
+ indirectly-triggered state changes, such as via a keyboard shortcut not
+ encoded in the keymap, to be reported after the done event.
A client must assume that all modifiers are lifted when it
receives an ei_device.paused event. The EIS implementation
@@ -1377,6 +1398,12 @@
be processed immediately by the client.
This event is only sent for devices with an ei_keyboard.keymap.
+
+ Note: A previous version of the documentation instead specified that
+ this event should not be sent in response to ei_keyboard.key events
+ that change the group or modifier state according to the keymap.
+ However, this complicated client implementation and resulted in
+ situations where the client state could get out of sync with the server.
diff --git a/src/libei.h b/src/libei.h
index a532143..1ac96a8 100644
--- a/src/libei.h
+++ b/src/libei.h
@@ -414,11 +414,35 @@ enum ei_event_type {
* ei_event_keyboard_get_xkb_mods_locked(), and
* ei_event_keyboard_get_xkb_group().
*
- * This event is sent in response to an external modifier state
- * change. Where the client triggers a modifier state change in
- * response to ei_device_keyboard_key(), no such event is sent.
+ * This event is sent in response to any modifier state or effective
+ * group change, including where the change is triggered by a client
+ * call to ei_device_keyboard_key().
+ *
+ * For receiver clients, this will always be properly ordered with
+ * EI_EVENT_KEYBOARD_KEY events, so each key event should be
+ * interpreted should using the most recently received modifier
+ * state.
+ *
+ * For sender clients, the this event is not inherently synchronized
+ * with calls to ei_device_keyboard_key(), but the client may call
+ * ei_ping() when synchronization is required. When the corresponding
+ * EI_EVENT_PONG event is received, all key events sent prior to the
+ * sync request are guaranteed to have been processed, and any
+ * directly-resulting modifiers events are guaranteed to have been
+ * received. Note, however, that it is still possible for
+ * indirectly-triggered state changes, such as via a keyboard
+ * shortcut not encoded in the keymap, to be reported after the done
+ * event.
*
* This event may arrive while a device is paused.
+ *
+ * Note: It was previously specified that a where a sender client
+ * triggers a modifier state change in response to
+ * ei_device_keyboard_key(), no MODIFIERS event would be sent.
+ * Clients were expected to mix calls to xkb_state_update_key() and
+ * xkb_state_update_mask() to track the state with libxkbcommon,
+ * which could lead to disagreements between the client and server as
+ * to the current state.
*/
EI_EVENT_KEYBOARD_MODIFIERS,
@@ -1995,7 +2019,20 @@ ei_event_keyboard_get_xkb_mods_locked(struct ei_event *event);
* @ingroup libei-receiver
*
* For an event of type @ref EI_EVENT_KEYBOARD_MODIFIERS, get the
- * logical group state.
+ * current effective group.
+ *
+ * This may be passed to xkb_state_update_mask() as either
+ * depressed_layout (effectively pretending the user is holding down some
+ * key for this group at all times) or locked_layout (treating it as a
+ * layout the user has switched to through some mechanism), but never
+ * both at the same time. The other two layout arguments must be set to
+ * zero.
+ *
+ * Note: Because the client only knows the current effective group and
+ * not the combination of state from which it was calculated, any attempt
+ * to predict how future key presses will impact the group state will
+ * necessarily be unreliable.
+ *
* See ei_device_keyboard_get_keymap() for the corresponding keymap.
*/
uint32_t
diff --git a/src/libeis.h b/src/libeis.h
index c85a75d..4654db4 100644
--- a/src/libeis.h
+++ b/src/libeis.h
@@ -1399,6 +1399,22 @@ eis_device_keyboard_get_keymap(struct eis_device *device);
* @ingroup libeis-device
*
* Notify the client of the current XKB modifier state.
+ *
+ * This should be called every time the modifier state or current
+ * effective group changes.
+ *
+ * When the state changes due to an incoming
+ * EIS_EVENT_KEYBOARD_KEY (for sender clients), this method should be
+ * called when the corresponding EIS_EVENT_FRAME is processed, before
+ * processing any subsequent events.
+ *
+ * When the state changes due to a key press with a receiver client, this
+ * method should be called immediately after the corresponding call to
+ * eis_device_frame(). If the change impacts multiple keyboards, this
+ * method should be called for all of them.
+ *
+ * For changes caused by other factors, this method should be called for
+ * all affected keyboards at the point the change occurs.
*/
void
eis_device_keyboard_send_xkb_modifiers(struct eis_device *device,