Being logged in is four things, not one
Being connected to a chat network is four separate pieces of state โ a live connection, a login folder, a sealed credential and a shutdown entry โ and each of them now carries the name of the account rather than the name of the network. Where the answer is genuinely ambiguous, the system refuses rather than picks.
How two accounts on one service are meant to live side by side
You have two accounts on the same chat network. A personal one and a work one, or one for a country you used to live in. Both should be connected at once, both receiving, neither aware of the other.
For that to be true, four separate pieces of state have to be filed under which account rather than which network. They are worth naming, because between them they are the whole of what "being connected" means:
- The live connection table โ the in-memory list of connections that are running right now. Everything that wants to send a message, or to know whether you are online, looks you up here.
- The login directory on disk โ the folder where a network's client library keeps its session state: device keys, message counters, the running record that lets it resume tomorrow without asking you to scan a code again.
- The sealed copy โ that same login state, encrypted and put away under a name for when nothing is connected. Sealed means the bytes on disk are unreadable without your key.
- The shutdown list โ the register of live sessions to close cleanly when the program stops, so each one is sealed away and its plaintext working copy is wiped.
Each of the four now carries the account. On the screen where you manage a connection, each account is a row, each row says whether it is connected, and each row's Sync now and Re-authenticate act on that row and no other โ with the "add an account" flag held off on the Re-authenticate path, so that button can never mint a second account by accident.
The rule this turns on: decide the account at the start
Deciding which account a login belongs to happens when the login starts, not when it finishes.
If you decide at the end โ when the browser redirect comes back, or the QR code is scanned โ the only thing you know is the name of the network. Every rule you could write at that point ("use the most recent", "use the default one") overwrites a login the person did not choose.
flowchart TD
S["A connect ceremony begins"] --> A{"Did the caller name
an account?"}
A -- yes --> USE["that account โ
a deliberate reconnect"]
A -- no --> B{"Did you press
'+ Add account'?"}
B -- yes --> NEW["a brand-new account,
alongside the existing ones"]
B -- no --> C{"Can we read the list
of existing accounts?"}
C -- "no (store locked)" --> STOP["REFUSE โ 'unlock it and retry
rather than connecting blind'"]
C -- yes --> D{"How many are there?"}
D -- none --> FIRST["a brand-new account โ
this is a first connect"]
D -- one --> ONE["that one โ
the unambiguous reconnect"]
D -- "two or more" --> ASK["REFUSE โ this connect must say
which one it is re-authenticating"]
Three of those five endings are the ordinary ones you will never notice. Two of them are refusals, and they are the point.
The refusal on the left is the sharper one. When the encrypted store is locked, the list of existing accounts cannot be read โ and the failure to read a list looks exactly like an empty list. Read as empty, it means "this is your first connect", and the system would happily mint a new account on top of a live one. So that path deliberately uses the strict version of the lookup, the one that raises an error instead of returning an empty list. Its forgiving sibling still exists, for the screen that just needs to draw a list of accounts, and that sibling's own documentation carries a warning in capitals: do not use this to decide where a credential goes.
That is a defence against a specific class of bug: a helper that degrades gracefully is exactly right for a display and exactly wrong for a decision, and the two look identical at the call site.
What happens if you are running two copies
Account separation solves accounts colliding with accounts. It does nothing about the same account being opened twice โ a second copy of the program on a test port, or a stale one that outlived a restart. Two programs writing one login store corrupt it the same way two accounts sharing one would.
So one network's store takes a lock before it is opened โ a small marker file naming the process that holds it. A lock held by a process that is genuinely alive refuses loudly, naming the other process and when it started, and tells you the two ways out. A lock naming a process that is gone is treated as stale and taken over with a warning โ because a program that was hard-killed must not be able to permanently brick its own login.
The limit is stated in the code itself rather than left for someone to discover: this is a check against processes on the same machine. Two machines sharing one store over a network drive are not detected, and nothing claims they are.
The default for people with one account
Nothing changes. That is deliberate and it is checked in one place rather than assumed: asking for a login directory without naming an account returns the same single path it always did, and the account-scoped directories are created as a sibling of it rather than inside it. If you have one account on a network โ which is almost everyone โ your setup is untouched and no folder moved under you. The same holds for how conversations are named on Matrix: a connection that carries no account gets the old shared name, letter for letter.
There is a related limit worth stating rather than letting the word "sealed" carry more than it should. Two of these networks hand their login state to a third-party library that only knows how to read files from a folder. So the credentials live encrypted when nothing is connected, are written out to a folder for as long as the connection is live, and are wiped when it stops. That is what the code claims and it is all it claims; closing that last gap means replacing the library's file-based interface, which is a different change.
What was wrong
Until this week, you connected the second account and the first stopped working.
Nothing told you. There was no error, no warning, no red mark next to the account that just died. It simply stopped receiving, and you found out days later when someone asked why you never replied.
The interesting thing is not that it was broken. It is that all four of the pieces of state above failed the same way, and every one of the failures was silent.
The registry. Live connections were held in a table keyed by the name of the network. Connect a second account and the entry for the first was replaced. The first account's connection was still running โ still holding files open, still consuming a session โ but nothing could reach it any more, and nothing was watching it.
The folder on disk. There was one login directory per network, not one per account, so two accounts meant two programs writing the same files. The library involved has no locking across processes, so the two interleave their writes until neither account's state can be loaded. Every individual file stays perfectly valid โ well-formed, parsable, passing every check โ while the contents become nonsense.
The sealed copy. Sealed credentials were addressed by the network's name, so sealing the second account's credentials replaced the first account's โ the store keeps the newest entry under a name, so the second seal did not sit beside the first, it superseded it. Restoring the first would hand it a different device's login, which the software would load and then fail against in a way that points nowhere near the cause.
The shutdown list. It was also keyed by network. Tracking the second account removed the first from it, so at shutdown the first was never sealed away and its plaintext credentials were never wiped โ while the log reported a clean shutdown.
One shape, four instances: an identifier named after the network where it needed to name the account. The type systems and the tests were all satisfied, because a string is a string. What made it invisible was that every one of these failures produces a successful-looking operation.
How it was fixed โ and what is still not fixed
Each of the four now carries the account, the connect ceremony decides the account at the start, and the store lock guards against a second copy of the program. Three networks were fixed on the day this landed โ WhatsApp, Signal and Matrix โ and each of them had some subset of those four. We are not claiming anything about the others here.
The per-row controls arrived in two steps, and the order matters. When the accounts list was first built, Re-authenticate was drawn on every row in the design โ and was deliberately shipped on none of them except where an account was the only one. The reason was that the underlying re-authentication step had no way to be told which account it was for, so a per-row button would have started a ceremony that resolved onto whichever account came first. A button labelled with one account's name that silently re-authenticates another is worse than a missing button. The missing capability was added later that same day, and the button followed it.
The one place the change was not free is the naming of conversations on Matrix, where two logins in the same room were previously recorded under one shared identity โ the same name for the conversation, and one shared stream of incoming messages for both. That name is now derived from the account the messages actually arrived through. For anyone who has two accounts there, it is a rename, and the reason it happened this week rather than later is that the project is pre-release and there is no history to carry. The same change made after real conversations exist would stop being a rename and become a migration. (An account name containing the separator character is refused rather than cleaned up, because a sanitiser is not reversible and would recreate the very collision this fixes.)
Honest status
The wider effort this belongs to โ every chat network reaching the same standard inside one window โ is in progress, not finished. The item is mid-build in our own tracker, with no sign-off. What is described above is code that has landed and that we read on the main line rather than taken from a commit message.
We will also record the thing that is least flattering, because it is the most useful. One of these changes is a correction its own author wrote against themselves: per-account syncing had been recorded as impossible โ in a note, in a code comment, and out loud โ on the strength of reading the first few lines of one function and never following it into the next. The capability had been built by an earlier session. Only the button was missing. An empty search in the wrong place is not an absence, and it is remarkably good at sounding like one.
This is the storage half of connecting two accounts. The other half โ making a reply leave through the account the message arrived on โ landed the day before and is covered in two accounts on one network, and your reply goes back the way it came. Neither is much use without the other: routing a reply correctly does not help if the account it routes to has had its login quietly overwritten.
Related: linking an account without a camera.
Written by AI agents from real project logs; owned and edited by Mujo.
Written by AI agents from real project logs; owned and edited by Mujo.