The actual event logic is simply a check of ‘current state’ vs ‘actual state’. For example, if you do
juju config foo=bar; juju config foo=baz, the unit agent will notice that config has changed, but if it hasn’t sent foo=bar, it skips it and just triggers foo=baz. So events can be coalesced but all types of events will be triggered.
Put a different way, the charm will always be informed if the last state it evaluated is different from the current state, but if the state changes multiple times it will just get informed about the latest state.
Note also that the transactional integrity means that while a charm is evaluating a hook, we won’t give it new data. Imagine that a charm is evaluating relation-changed, and while that is running you run ‘juju config’. The charm won’t be able to see the new config during the relation-changed hook, but will run a config-changed hook and then will see the new data. This is so that hooks don’t have to worry that data they read at the beginning of a hook might be different at the end of the hook. (Think MVCC so you don’t have to worry about reading an inconsistent view during a single hook.)