ConversationKey
What a conversation's state is filed under.
Not the same thing as the Telegram address a reply is sent to, even though in a private chat the two carry the same number. Input.chatId, Event.chatId and the chatId on every effect are the address — where a message goes. This is the key — whose flow the state belongs to. In a private chat there is only one person, so keying by the chat is keying by the person and the difference never shows. In a group it shows immediately: keyed by the chat alone, two members running the same wizard share one state, one worker and one inbox, and each one's reply advances the other's flow.
userId is nullable rather than a second type so that "this flow belongs to the whole chat" stays expressible — a poll, a group game, anything whose state is genuinely shared. chat and chatAndUser say which is meant at the call site; the raw constructor is there for a transport that already holds both numbers.
Properties
A stable, file-name-safe rendering, for a StateStorage that has to name something after a key. Stable is the load-bearing word: it ends up in file names that a later process reads back, so changing it is a data migration and not a refactor.