fix(matrix): E2EE and migration bugfixes - #10860
Conversation
- fix path naming to expand `~` for agent. - fix stripping of matrix ID to not remove other mentions / localports.
The mautrix migration (#7518) broke auto-join because InternalEventType.INVITE events are only dispatched when MembershipEventDispatcher is registered on the client. Without it, _on_invite is dead code and the bot silently ignores all room invites. Closes #10094 Closes #10725 Refs: PR #10135 (digging-airfare-4u), PR #10732 (fxfitz)
connect() reassigned self._joined_rooms = set(...) after initial sync, orphaning the reference captured by _CryptoStateStore at init time. find_shared_rooms() returned [] forever, breaking Megolm session rotation on membership changes. Mutate in place with clear() + update() so the CryptoStateStore reference stays valid. Refs #8174, PR #8215
mautrix auto-registers DecryptionDispatcher when client.crypto is set. The adapter also registered _on_encrypted_event for the same event type. _on_encrypted_event had zero awaits and won the race to mark event IDs in the dedup set, causing _on_room_message to drop successfully decrypted events from DecryptionDispatcher. The retry loop masked this by re-decrypting every message ~4 seconds later. Remove _on_encrypted_event entirely. DecryptionDispatcher handles decryption; genuinely undecryptable events are logged by mautrix and retried on next key exchange. Refs #8174, PR #8215
Matrix homeservers treat ed25519 identity keys as immutable per device. share_keys() can return 200 but silently ignore new keys if the device already exists with different identity keys. The bot would proceed with shared=True while peers encrypt to the old (unreachable) keys. Now re-queries the server after share_keys() and fails closed if keys don't match, with an actionable error message. Refs #8174, PR #8215
_upload_and_send() uploaded raw bytes and used the 'url' key for all rooms. In E2EE rooms, media must be encrypted client-side with encrypt_attachment(), the ciphertext uploaded, and the 'file' key (with key/iv/hashes) used instead of 'url'. Now detects encrypted rooms via state_store.is_encrypted() and branches to the encrypted upload path. Refs: PR #9822 (charles-brooks)
When crypto state is wiped but the same device ID is reused, the homeserver may still hold one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with "already exists" and a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are undecryptable. Now proactively flushes OTKs via share_keys() during connect() and catches the "already exists" error with an actionable log message telling the operator to purge the device from the homeserver or generate a fresh device ID. Also documents the crypto store recovery procedure in the Matrix setup guide. Refs #8174
- Put easy path (fresh access token) first, manual purge second - URL-encode user ID in Synapse admin API example - Note that device deletion may invalidate the access token - Add "stop Synapse first" caveat for direct SQLite approach - Mention the fail-closed startup detection behavior - Add back-reference from upgrade section to OTK warning
# Conflicts: # gateway/platforms/matrix.py
- Extract _extract_server_ed25519() and _reverify_keys_after_upload() to deduplicate the re-verification block (was copy-pasted in two places, three copies of ed25519 key extraction total) - Remove dead code: _pending_megolm, _retry_pending_decryptions, _MAX_PENDING_EVENTS, _PENDING_EVENT_TTL — all orphaned after removing _on_encrypted_event - Remove tautological TestMediaCacheGate (tested its own predicate, not production code) - Remove dead TestMatrixMegolmEventHandling and TestMatrixRetryPendingDecryptions (tested removed methods) - Merge duplicate TestMatrixStopTyping into TestMatrixTypingIndicator - Trim comment to just the "why"
|
Hi @alt-glitch, Thanks for the massive effort on the Matrix rewrite. I've just tested the latest version on Debian. The good news: The MembershipEventDispatcher fix is working. The bot now successfully auto-joins rooms upon invitation. The bad news: The bot remains silent and does not respond to messages. Even with MATRIX_ENCRYPTION=false and a completely wiped crypto.db/store, there are no "inbound message" logs in --debug mode when a message is sent in an unencrypted room. How can I solve this problem? |
|
@Schnurzel700 For me it works now, even in encrypted rooms. So it seems something else might be broken with your bot account. |
|
@alt-glitch I've ruled out all local state issues: Setup: Completely fresh Debian VM & brand new Matrix.org account. The Bug: Bot joins via invite, but zero console output in --debug when messaging the bot. No Received event, no inbound message. Symptoms: Sync loop seems to hang or disconnect after ~30s of silence. It appears the message handler isn't being bound to the sync loop in the new mautrix implementation. The bot is effectively "deaf" on clean installs. |
|
I found the issue! |
Yes please... I went crazy with a lot of forth and back |
Re-applied on top of clean origin/main after iAdrian branch reset: .gitea/workflows/: - sync-from-github.yml — pulls main from GitHub fork into Gitea every 10min - docker-build.yml — builds + pushes hermes-agent image to git.ciolan.net - tests.yml — pytest on PR/push (mirrors GitHub tests.yml subset) - nix.yml — no-op override to prevent upstream nix CI on Gitea .github/workflows/: - sync-upstream.yml — pulls NousResearch/hermes-agent main into iAdrian fork every 10min Replaces the 18 incremental CI commits from the old iAdrian history (all squashed into this single clean commit). All upstream code-level patches (matrix E2EE, Dockerfile-git, channel_directory) are obsolete: upstream shipped equivalent or better fixes (PRs NousResearch#10860, NousResearch#7450), so iAdrian now tracks main exactly minus this CI overlay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* - make buffered streaming - fix path naming to expand `~` for agent. - fix stripping of matrix ID to not remove other mentions / localports. * fix(matrix): register MembershipEventDispatcher for invite auto-join The mautrix migration (NousResearch#7518) broke auto-join because InternalEventType.INVITE events are only dispatched when MembershipEventDispatcher is registered on the client. Without it, _on_invite is dead code and the bot silently ignores all room invites. Closes NousResearch#10094 Closes NousResearch#10725 Refs: PR NousResearch#10135 (digging-airfare-4u), PR NousResearch#10732 (fxfitz) * fix(matrix): preserve _joined_rooms reference for CryptoStateStore connect() reassigned self._joined_rooms = set(...) after initial sync, orphaning the reference captured by _CryptoStateStore at init time. find_shared_rooms() returned [] forever, breaking Megolm session rotation on membership changes. Mutate in place with clear() + update() so the CryptoStateStore reference stays valid. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): remove dual ROOM_ENCRYPTED handler to fix dedup race mautrix auto-registers DecryptionDispatcher when client.crypto is set. The adapter also registered _on_encrypted_event for the same event type. _on_encrypted_event had zero awaits and won the race to mark event IDs in the dedup set, causing _on_room_message to drop successfully decrypted events from DecryptionDispatcher. The retry loop masked this by re-decrypting every message ~4 seconds later. Remove _on_encrypted_event entirely. DecryptionDispatcher handles decryption; genuinely undecryptable events are logged by mautrix and retried on next key exchange. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): re-verify device keys after share_keys() upload Matrix homeservers treat ed25519 identity keys as immutable per device. share_keys() can return 200 but silently ignore new keys if the device already exists with different identity keys. The bot would proceed with shared=True while peers encrypt to the old (unreachable) keys. Now re-queries the server after share_keys() and fails closed if keys don't match, with an actionable error message. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): encrypt outbound attachments in E2EE rooms _upload_and_send() uploaded raw bytes and used the 'url' key for all rooms. In E2EE rooms, media must be encrypted client-side with encrypt_attachment(), the ciphertext uploaded, and the 'file' key (with key/iv/hashes) used instead of 'url'. Now detects encrypted rooms via state_store.is_encrypted() and branches to the encrypted upload path. Refs: PR NousResearch#9822 (charles-brooks) * fix(matrix): add stop_typing to clear typing indicator after response The adapter set a 30-second typing timeout but never cleared it. The base class stop_typing() is a no-op, so the typing indicator lingered for up to 30 seconds after each response. Closes NousResearch#6016 Refs: PR NousResearch#6020 (r266-tech) * fix(matrix): cache all media types locally, not just photos/voice should_cache_locally only covered PHOTO, VOICE, and encrypted media. Unencrypted audio/video/documents in plaintext rooms were passed as MXC URLs that require authentication the agent doesn't have, resulting in 401 errors. Refs NousResearch#3487, NousResearch#3806 * fix(matrix): detect stale OTK conflict on startup and fail closed When crypto state is wiped but the same device ID is reused, the homeserver may still hold one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with "already exists" and a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are undecryptable. Now proactively flushes OTKs via share_keys() during connect() and catches the "already exists" error with an actionable log message telling the operator to purge the device from the homeserver or generate a fresh device ID. Also documents the crypto store recovery procedure in the Matrix setup guide. Refs NousResearch#8174 * docs(matrix): improve crypto recovery docs per review - Put easy path (fresh access token) first, manual purge second - URL-encode user ID in Synapse admin API example - Note that device deletion may invalidate the access token - Add "stop Synapse first" caveat for direct SQLite approach - Mention the fail-closed startup detection behavior - Add back-reference from upgrade section to OTK warning * refactor(matrix): cleanup from code review - Extract _extract_server_ed25519() and _reverify_keys_after_upload() to deduplicate the re-verification block (was copy-pasted in two places, three copies of ed25519 key extraction total) - Remove dead code: _pending_megolm, _retry_pending_decryptions, _MAX_PENDING_EVENTS, _PENDING_EVENT_TTL — all orphaned after removing _on_encrypted_event - Remove tautological TestMediaCacheGate (tested its own predicate, not production code) - Remove dead TestMatrixMegolmEventHandling and TestMatrixRetryPendingDecryptions (tested removed methods) - Merge duplicate TestMatrixStopTyping into TestMatrixTypingIndicator - Trim comment to just the "why"
* - make buffered streaming - fix path naming to expand `~` for agent. - fix stripping of matrix ID to not remove other mentions / localports. * fix(matrix): register MembershipEventDispatcher for invite auto-join The mautrix migration (NousResearch#7518) broke auto-join because InternalEventType.INVITE events are only dispatched when MembershipEventDispatcher is registered on the client. Without it, _on_invite is dead code and the bot silently ignores all room invites. Closes NousResearch#10094 Closes NousResearch#10725 Refs: PR NousResearch#10135 (digging-airfare-4u), PR NousResearch#10732 (fxfitz) * fix(matrix): preserve _joined_rooms reference for CryptoStateStore connect() reassigned self._joined_rooms = set(...) after initial sync, orphaning the reference captured by _CryptoStateStore at init time. find_shared_rooms() returned [] forever, breaking Megolm session rotation on membership changes. Mutate in place with clear() + update() so the CryptoStateStore reference stays valid. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): remove dual ROOM_ENCRYPTED handler to fix dedup race mautrix auto-registers DecryptionDispatcher when client.crypto is set. The adapter also registered _on_encrypted_event for the same event type. _on_encrypted_event had zero awaits and won the race to mark event IDs in the dedup set, causing _on_room_message to drop successfully decrypted events from DecryptionDispatcher. The retry loop masked this by re-decrypting every message ~4 seconds later. Remove _on_encrypted_event entirely. DecryptionDispatcher handles decryption; genuinely undecryptable events are logged by mautrix and retried on next key exchange. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): re-verify device keys after share_keys() upload Matrix homeservers treat ed25519 identity keys as immutable per device. share_keys() can return 200 but silently ignore new keys if the device already exists with different identity keys. The bot would proceed with shared=True while peers encrypt to the old (unreachable) keys. Now re-queries the server after share_keys() and fails closed if keys don't match, with an actionable error message. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): encrypt outbound attachments in E2EE rooms _upload_and_send() uploaded raw bytes and used the 'url' key for all rooms. In E2EE rooms, media must be encrypted client-side with encrypt_attachment(), the ciphertext uploaded, and the 'file' key (with key/iv/hashes) used instead of 'url'. Now detects encrypted rooms via state_store.is_encrypted() and branches to the encrypted upload path. Refs: PR NousResearch#9822 (charles-brooks) * fix(matrix): add stop_typing to clear typing indicator after response The adapter set a 30-second typing timeout but never cleared it. The base class stop_typing() is a no-op, so the typing indicator lingered for up to 30 seconds after each response. Closes NousResearch#6016 Refs: PR NousResearch#6020 (r266-tech) * fix(matrix): cache all media types locally, not just photos/voice should_cache_locally only covered PHOTO, VOICE, and encrypted media. Unencrypted audio/video/documents in plaintext rooms were passed as MXC URLs that require authentication the agent doesn't have, resulting in 401 errors. Refs NousResearch#3487, NousResearch#3806 * fix(matrix): detect stale OTK conflict on startup and fail closed When crypto state is wiped but the same device ID is reused, the homeserver may still hold one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with "already exists" and a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are undecryptable. Now proactively flushes OTKs via share_keys() during connect() and catches the "already exists" error with an actionable log message telling the operator to purge the device from the homeserver or generate a fresh device ID. Also documents the crypto store recovery procedure in the Matrix setup guide. Refs NousResearch#8174 * docs(matrix): improve crypto recovery docs per review - Put easy path (fresh access token) first, manual purge second - URL-encode user ID in Synapse admin API example - Note that device deletion may invalidate the access token - Add "stop Synapse first" caveat for direct SQLite approach - Mention the fail-closed startup detection behavior - Add back-reference from upgrade section to OTK warning * refactor(matrix): cleanup from code review - Extract _extract_server_ed25519() and _reverify_keys_after_upload() to deduplicate the re-verification block (was copy-pasted in two places, three copies of ed25519 key extraction total) - Remove dead code: _pending_megolm, _retry_pending_decryptions, _MAX_PENDING_EVENTS, _PENDING_EVENT_TTL — all orphaned after removing _on_encrypted_event - Remove tautological TestMediaCacheGate (tested its own predicate, not production code) - Remove dead TestMatrixMegolmEventHandling and TestMatrixRetryPendingDecryptions (tested removed methods) - Merge duplicate TestMatrixStopTyping into TestMatrixTypingIndicator - Trim comment to just the "why"
* - make buffered streaming - fix path naming to expand `~` for agent. - fix stripping of matrix ID to not remove other mentions / localports. * fix(matrix): register MembershipEventDispatcher for invite auto-join The mautrix migration (NousResearch#7518) broke auto-join because InternalEventType.INVITE events are only dispatched when MembershipEventDispatcher is registered on the client. Without it, _on_invite is dead code and the bot silently ignores all room invites. Closes NousResearch#10094 Closes NousResearch#10725 Refs: PR NousResearch#10135 (digging-airfare-4u), PR NousResearch#10732 (fxfitz) * fix(matrix): preserve _joined_rooms reference for CryptoStateStore connect() reassigned self._joined_rooms = set(...) after initial sync, orphaning the reference captured by _CryptoStateStore at init time. find_shared_rooms() returned [] forever, breaking Megolm session rotation on membership changes. Mutate in place with clear() + update() so the CryptoStateStore reference stays valid. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): remove dual ROOM_ENCRYPTED handler to fix dedup race mautrix auto-registers DecryptionDispatcher when client.crypto is set. The adapter also registered _on_encrypted_event for the same event type. _on_encrypted_event had zero awaits and won the race to mark event IDs in the dedup set, causing _on_room_message to drop successfully decrypted events from DecryptionDispatcher. The retry loop masked this by re-decrypting every message ~4 seconds later. Remove _on_encrypted_event entirely. DecryptionDispatcher handles decryption; genuinely undecryptable events are logged by mautrix and retried on next key exchange. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): re-verify device keys after share_keys() upload Matrix homeservers treat ed25519 identity keys as immutable per device. share_keys() can return 200 but silently ignore new keys if the device already exists with different identity keys. The bot would proceed with shared=True while peers encrypt to the old (unreachable) keys. Now re-queries the server after share_keys() and fails closed if keys don't match, with an actionable error message. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): encrypt outbound attachments in E2EE rooms _upload_and_send() uploaded raw bytes and used the 'url' key for all rooms. In E2EE rooms, media must be encrypted client-side with encrypt_attachment(), the ciphertext uploaded, and the 'file' key (with key/iv/hashes) used instead of 'url'. Now detects encrypted rooms via state_store.is_encrypted() and branches to the encrypted upload path. Refs: PR NousResearch#9822 (charles-brooks) * fix(matrix): add stop_typing to clear typing indicator after response The adapter set a 30-second typing timeout but never cleared it. The base class stop_typing() is a no-op, so the typing indicator lingered for up to 30 seconds after each response. Closes NousResearch#6016 Refs: PR NousResearch#6020 (r266-tech) * fix(matrix): cache all media types locally, not just photos/voice should_cache_locally only covered PHOTO, VOICE, and encrypted media. Unencrypted audio/video/documents in plaintext rooms were passed as MXC URLs that require authentication the agent doesn't have, resulting in 401 errors. Refs NousResearch#3487, NousResearch#3806 * fix(matrix): detect stale OTK conflict on startup and fail closed When crypto state is wiped but the same device ID is reused, the homeserver may still hold one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with "already exists" and a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are undecryptable. Now proactively flushes OTKs via share_keys() during connect() and catches the "already exists" error with an actionable log message telling the operator to purge the device from the homeserver or generate a fresh device ID. Also documents the crypto store recovery procedure in the Matrix setup guide. Refs NousResearch#8174 * docs(matrix): improve crypto recovery docs per review - Put easy path (fresh access token) first, manual purge second - URL-encode user ID in Synapse admin API example - Note that device deletion may invalidate the access token - Add "stop Synapse first" caveat for direct SQLite approach - Mention the fail-closed startup detection behavior - Add back-reference from upgrade section to OTK warning * refactor(matrix): cleanup from code review - Extract _extract_server_ed25519() and _reverify_keys_after_upload() to deduplicate the re-verification block (was copy-pasted in two places, three copies of ed25519 key extraction total) - Remove dead code: _pending_megolm, _retry_pending_decryptions, _MAX_PENDING_EVENTS, _PENDING_EVENT_TTL — all orphaned after removing _on_encrypted_event - Remove tautological TestMediaCacheGate (tested its own predicate, not production code) - Remove dead TestMatrixMegolmEventHandling and TestMatrixRetryPendingDecryptions (tested removed methods) - Merge duplicate TestMatrixStopTyping into TestMatrixTypingIndicator - Trim comment to just the "why"
* - make buffered streaming - fix path naming to expand `~` for agent. - fix stripping of matrix ID to not remove other mentions / localports. * fix(matrix): register MembershipEventDispatcher for invite auto-join The mautrix migration (NousResearch#7518) broke auto-join because InternalEventType.INVITE events are only dispatched when MembershipEventDispatcher is registered on the client. Without it, _on_invite is dead code and the bot silently ignores all room invites. Closes NousResearch#10094 Closes NousResearch#10725 Refs: PR NousResearch#10135 (digging-airfare-4u), PR NousResearch#10732 (fxfitz) * fix(matrix): preserve _joined_rooms reference for CryptoStateStore connect() reassigned self._joined_rooms = set(...) after initial sync, orphaning the reference captured by _CryptoStateStore at init time. find_shared_rooms() returned [] forever, breaking Megolm session rotation on membership changes. Mutate in place with clear() + update() so the CryptoStateStore reference stays valid. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): remove dual ROOM_ENCRYPTED handler to fix dedup race mautrix auto-registers DecryptionDispatcher when client.crypto is set. The adapter also registered _on_encrypted_event for the same event type. _on_encrypted_event had zero awaits and won the race to mark event IDs in the dedup set, causing _on_room_message to drop successfully decrypted events from DecryptionDispatcher. The retry loop masked this by re-decrypting every message ~4 seconds later. Remove _on_encrypted_event entirely. DecryptionDispatcher handles decryption; genuinely undecryptable events are logged by mautrix and retried on next key exchange. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): re-verify device keys after share_keys() upload Matrix homeservers treat ed25519 identity keys as immutable per device. share_keys() can return 200 but silently ignore new keys if the device already exists with different identity keys. The bot would proceed with shared=True while peers encrypt to the old (unreachable) keys. Now re-queries the server after share_keys() and fails closed if keys don't match, with an actionable error message. Refs NousResearch#8174, PR NousResearch#8215 * fix(matrix): encrypt outbound attachments in E2EE rooms _upload_and_send() uploaded raw bytes and used the 'url' key for all rooms. In E2EE rooms, media must be encrypted client-side with encrypt_attachment(), the ciphertext uploaded, and the 'file' key (with key/iv/hashes) used instead of 'url'. Now detects encrypted rooms via state_store.is_encrypted() and branches to the encrypted upload path. Refs: PR NousResearch#9822 (charles-brooks) * fix(matrix): add stop_typing to clear typing indicator after response The adapter set a 30-second typing timeout but never cleared it. The base class stop_typing() is a no-op, so the typing indicator lingered for up to 30 seconds after each response. Closes NousResearch#6016 Refs: PR NousResearch#6020 (r266-tech) * fix(matrix): cache all media types locally, not just photos/voice should_cache_locally only covered PHOTO, VOICE, and encrypted media. Unencrypted audio/video/documents in plaintext rooms were passed as MXC URLs that require authentication the agent doesn't have, resulting in 401 errors. Refs NousResearch#3487, NousResearch#3806 * fix(matrix): detect stale OTK conflict on startup and fail closed When crypto state is wiped but the same device ID is reused, the homeserver may still hold one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with "already exists" and a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are undecryptable. Now proactively flushes OTKs via share_keys() during connect() and catches the "already exists" error with an actionable log message telling the operator to purge the device from the homeserver or generate a fresh device ID. Also documents the crypto store recovery procedure in the Matrix setup guide. Refs NousResearch#8174 * docs(matrix): improve crypto recovery docs per review - Put easy path (fresh access token) first, manual purge second - URL-encode user ID in Synapse admin API example - Note that device deletion may invalidate the access token - Add "stop Synapse first" caveat for direct SQLite approach - Mention the fail-closed startup detection behavior - Add back-reference from upgrade section to OTK warning * refactor(matrix): cleanup from code review - Extract _extract_server_ed25519() and _reverify_keys_after_upload() to deduplicate the re-verification block (was copy-pasted in two places, three copies of ed25519 key extraction total) - Remove dead code: _pending_megolm, _retry_pending_decryptions, _MAX_PENDING_EVENTS, _PENDING_EVENT_TTL — all orphaned after removing _on_encrypted_event - Remove tautological TestMediaCacheGate (tested its own predicate, not production code) - Remove dead TestMatrixMegolmEventHandling and TestMatrixRetryPendingDecryptions (tested removed methods) - Merge duplicate TestMatrixStopTyping into TestMatrixTypingIndicator - Trim comment to just the "why"
Closes #10094, #10725, #6016, #3487, #3806
Summary
Fixes 7 E2EE and migration bugs in the Matrix adapter, adds startup detection for stale one-time key conflicts, and includes several quality-of-life improvements for Matrix streaming and mention handling.
All fixes are E2E tested against a local Synapse homeserver with encrypted rooms.
Other improvements
Buffered streaming for Matrix
Matrix clients (Element, etc.) render the streaming cursor (
▉) as a visible tofu artifact. The stream consumer now uses abuffer_onlymode for Matrix that suppresses intermediate edit-based updates — text is batched and sent as complete messages instead of rapid edits that hit rate limits.Fix
~expansion in media file pathsMEDIAtags emitted by the agent with paths like~/media/file.pngwere not expanded. Now callsos.path.expanduser()before attempting to read the file.Fix mention stripping to not mangle file paths
The old
_strip_mentionremoved both the full MXID (@hermes:server) and the bare localpart (hermes) from message bodies. Stripping the localpart mangled file paths like/home/hermes/media/file.png→/home//media/file.png. Now only strips the full MXID.E2EE Bugs Fixed
1. Auto-join broken — MembershipEventDispatcher never registered
Closes: #10094, #10725 | Refs: #10135, #10732
mautrix delivers room invites as raw
ROOM_MEMBERstate events. TheMembershipEventDispatcherthat converts them intoInternalEventType.INVITEwas never registered on the client, so the_on_invitehandler was dead code. The bot silently ignored all room invites.Fix: Register
MembershipEventDispatcherviaclient.add_dispatcher()inconnect().2.
_CryptoStateStorereference orphaned after initial syncRefs: #8174, #8215
connect()reassignedself._joined_rooms = set(rooms_join.keys())after initial sync, orphaning the reference captured by_CryptoStateStoreat init time.find_shared_rooms()returned[]forever, breaking Megolm session rotation on membership changes.Fix: Mutate in place with
clear()+update()instead of reassignment.3. Dual
ROOM_ENCRYPTEDhandler causes dedup raceRefs: #8174, #8215
Both mautrix's auto-registered
DecryptionDispatcherand hermes's_on_encrypted_eventfired for everyROOM_ENCRYPTEDevent._on_encrypted_eventwon the race (zero awaits), marked event IDs in the dedup set, and the successfully-decrypted event fromDecryptionDispatchergot dropped by_on_room_message's dedup check. The retry loop masked this by re-decrypting every message ~4 seconds later with spurious "could not decrypt" warnings.Fix: Remove
_on_encrypted_evententirely.DecryptionDispatcherhandles decryption; the retry loop and dedup-discard hack are no longer needed.4. No re-verification after
share_keys()uploadRefs: #8174, #8215
After calling
share_keys(), the method returnedTruewithout verifying the server actually accepted the new keys. Matrix homeservers treat ed25519 identity keys as immutable per device —share_keys()returns 200 but silently ignores new keys if the device already exists with different identity keys.Fix: Re-query the server after
share_keys()and fail closed if keys don't match, with an actionable error message.5. Outbound media sent unencrypted in E2EE rooms
Refs: #9822
_upload_and_send()uploaded raw bytes and used theurlkey for all rooms. In E2EE rooms, media must be encrypted client-side withencrypt_attachment(), the ciphertext uploaded, and thefilekey (withkey/iv/hashes) used instead ofurl.Fix: Detect encrypted rooms via
state_store.is_encrypted()and branch to the encrypted upload path usingmautrix.crypto.attachments.encrypt_attachment().6. Typing indicator lingers for 30 seconds after response
Closes: #6016 | Refs: #6020
send_typing()calledset_typing(timeout=30000)but the base classstop_typing()was a no-op. The typing indicator lingered for up to 30 seconds after each response.Fix: Override
stop_typing()to callset_typing(timeout=0).7. Audio/video/document files not cached locally
Closes: #3487, #3806
should_cache_locallyonly coveredPHOTO,VOICE, and encrypted media. Unencrypted audio/video/documents in plaintext rooms were passed as MXC URLs requiring authentication the agent doesn't have, resulting in 401 errors.Fix: Extend the gate to include
AUDIO,VIDEO, andDOCUMENTmessage types.8. Stale one-time key conflict after crypto state recovery
Refs: #8174
When crypto state is wiped but the same device ID is reused, the homeserver still holds one-time keys signed with the previous identity key. Identity key re-upload succeeds but OTK uploads fail with a signature mismatch. Peers cannot establish new Olm sessions, so all new messages are silently undecryptable.
Fix: Proactively flush OTKs via
share_keys()duringconnect()and catch the "already exists" error, refusing E2EE with an actionable log message. Also documents the crypto recovery procedure in the Matrix setup guide.Test plan
Unit tests
TestJoinedRoomsReference,TestDeviceKeyReVerification,TestMatrixUploadAndSend,TestMatrixEncryptedEventHandler::test_connect_fails_on_stale_otk_conflictE2E testing methodology
All fixes were verified end-to-end against a live Matrix environment:
Infrastructure:
zephyr), accessible via Tailscale atzephyr.giraffe-octatonic.ts.net:8008@hermes:zephyr.giraffe-octatonic.ts.net— runs as a NixOS container service with E2EE enabled via mautrixOlmMachine@sid, used to send encrypted messages, files, and verify bot responses programmaticallym.megolm.v1.aes-sha2encryption presetTest flow for each fix:
nixos-rebuild switch && systemctl restart hermes-agent)matrix-commanderand verify bot behaviorjournalctl -u hermes-agentfor errors/warningsE2E tests performed:
/membersAPI)crypto.db, purged device from Synapse DB, re-registered with fresh credentials, verified all fixes work from a completely clean slate