- `_auto_rename_room` now runs as a tracked bg task. Title generation
latency no longer affects when the handler returns and frees the
room lock, so users can fire the next message sooner.
- Memory extraction guards against providers returning `None` for
`choices[0].message.content` (observed in logs: AttributeError on
.strip). Logs once and returns cleanly instead of raising.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single info line per _stream_chat_completion call reporting model,
character count, tool-call count, and whether a Matrix message was
progressively streamed. Lets us confirm the streaming path is active
without tracing sentry breadcrumbs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Latency was dominated by the LLM call chain, not the 10-message context window.
Three fixes land together in the chat pipeline in bot.py:
1. Stream the main LLM call (new _stream_chat_completion helper) and
progressively edit the Matrix message via m.replace. Suppress visible
streaming during tool-calling iterations so the user never sees rolled-back
text. Final send is an authoritative edit that guarantees the full reply.
2. Gate _rewrite_query behind a pronoun/deictic heuristic (EN/DE/FR). When a
message has no references needing resolution we skip the extra Haiku
round-trip entirely and feed the original message to RAG directly.
3. Fire-and-forget the post-reply memory + chunk persistence with asyncio
background tasks so a slow extraction no longer blocks the next inbound
message. 20s timeout preserved inside the bg task; exceptions logged.
Added unit test for the pronoun heuristic (EN/DE/FR positive + negative cases,
short/empty messages).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unverified devices (lacking cross-signing) caused OlmUnverifiedDeviceError
in _send_text(), silently breaking all message delivery. Now on_sync()
blacklists non-cross-signed devices instead of skipping them, and
_send_text() catches E2EE errors gracefully.
Adds 12 unit tests for device trust policy and send error handling.
CI test job now gates deployment in deploy.yml.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
matrix-nio's DeviceStore has no .get() method. Use correct API:
DeviceStore[user_id].get(device_id) returns OlmDevice from inner dict.
Also fix keys_claim() to accept Dict[str, List[str]] per nio API.
This was the root cause of Element X silence — bot couldn't deliver
its E2EE key via Olm-encrypted to-device messages, so Element X
couldn't decrypt bot's audio frames.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /notify: send encrypted message to any room
- GET /messages: read decrypted messages from any room
- GET /health: health check
- Authenticated via BOT_API_KEY header
- Port 9100 exposed in docker-compose
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element X only reads encryption keys from encrypted to-device
messages, not room events or call.member state. Bot now sends
its key via Olm-encrypted to-device to all call participants,
matching Element Call's encryptAndSendToDevice behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The custom HKDF=1 path in the Rust FFI fork (EC-compat-changes)
produces different derived keys than the JS SDK's libwebrtc C++.
Switching to PBKDF2=0 lets libwebrtc's built-in C++ FrameCryptor
handle key derivation identically to how the JS SDK does it.
Also aligned ratchet_window_size=0 and key_ring_size=16 to match
Element Call JS SDK defaults (were 10 and 256).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element X sends keys as a single dict {index, key} not a list
[{index, key}]. The handler iterated over dict keys ('index','key')
instead of the actual key data. Also extracts device_id from
member.claimed_device_id (Element X format).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nio's _handle_olm_event silently drops unknown Olm event types
with 'Received unsupported Olm event'. Element X sends E2EE call
keys as encrypted to-device io.element.call.encryption_keys events.
After Olm decryption they were dropped before reaching any callback.
Patch intercepts the drop and forwards to on_to_device_unknown handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element X sends E2EE keys via encrypted to-device messages targeting
the device_id from the call.member state event. Bot was advertising
device_id='AIBOT' but its actual Matrix session is on device
'PEYRKFEXFP'. Keys were sent to a non-existent device.
Now uses the real device_id from nio credentials so Element X's
encryptAndSendToDevice reaches the correct device.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git pull + docker compose restart bot now works without --build.
Patched FFI binary and proto bindings stay in the image; only
Python source files are mounted read-only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element X (26.03.3+) sends io.element.call.encryption_keys as
to-device messages, not room timeline events. Added
UnknownToDeviceEvent callback to catch these and deliver keys
to active voice sessions.
Also added m.room.encrypted decryption attempt in timeline scan
as fallback for older Element versions that send encrypted timeline
events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element X embeds E2EE keys inside memberships[].encryption_keys,
not at the top level of the call.member state event content.
Bot was only checking content.encryption_keys, so it never found
the caller's key — causing 'Warten auf Medien' (waiting for media)
because encrypted audio couldn't be decrypted.
- Added _extract_enc_keys_from_content() helper handling both formats
- Updated on_unknown handler, VoiceSession creation, and key fetch
- Bot now publishes keys in both formats for compatibility
- Updated voice.py state fetch to check memberships[] fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Element Call v0.17+ embeds encryption_keys in call.member state events
instead of separate timeline events. In E2EE rooms, timeline events are
encrypted and the bot HTTP fetch cannot decrypt them, causing DEC_FAILED.
- Extract caller keys from call.member state event on join
- Embed bot key in call.member state event
- Check call.member state in key fetch (before timeline fallback)
- Handle key updates in call.member during active calls
- Update voice.py key poller to check call.member state first
- Add debug logging for UnknownEvent types in call rooms
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as files: download and cache in _recent_images without
responding. When user next @mentions the bot, the cached image is
available as context. Applied to both plain and encrypted image handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files uploaded to group rooms are now downloaded, parsed, and stored
in _room_document_context even without @mention. When the user later
mentions the bot, the document context is automatically included.
Previously files were silently dropped if the caption didn't contain
a mention, so the bot would say it can't access uploaded PDFs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bot user is @ai:agiliton.eu but display name is 'Claude'.
Element renders mentions using display name, so the old check
for 'ai' in message body never matched '@Claude: ...' messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add pitrader_script executor for running PITrader scripts (pi-scan,
playbook, execute_trades) as pipeline steps with vault credential
injection and JSON output capture.
Extend claude_prompt step with vision support (image_b64 in trigger
context). Add image pipeline trigger to on_image_message handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route HTTP-fetched keys through on_encryption_key() for proper rotation detection
- Replace boolean refetch gate with 500ms timestamp throttle for faster recovery
- Reduce DEC_FAILED cooldown from 2s to 0.5s
- Extend proactive key poll from 3s to 10s window
- Add continuous background key poller (3s interval) during active calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renders extracted items as markdown links with details instead of
raw JSON. Handles common patterns: list of dicts with title/link.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track running job IDs to avoid creating duplicate Skyvern tasks
when the pending check runs faster than the task completes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cloud API uses 'prompt', self-hosted uses 'navigation_goal' and
'data_extraction_goal'. Pass them separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>