# Plan: BasePlatformAdapter-Overrides nachziehen (Punkte 1–8) Referenz: `gateway/platforms/base.py` aus dem Hermes-Checkout (HEAD `7b25941`, identisch mit github.com/nousresearch/hermes-agent `main`). Jeder Punkt: was die Basis anbietet, was chattolib kann, was implementiert wird. --- ## 1. `edit_message` — base.py:4035, Default `SendResult(success=False, "Not supported")` chattolib: `client.update_message(room_id, event_id, body=...)`. Aufrufer: `gateway/stream_consumer.py:412` (Zeilen 1451, 1824, 2233) und `gateway/run.py:4530/28592/29493`. Solange `False` zurückkommt, wird jedes Streaming-Update als *neue* Nachricht gesendet. - Signatur exakt wie Basis, inkl. `finalize: bool = False` (für Chatto ein No-op — kein Rich-Card-Lifecycle, `REQUIRES_EDIT_FINALIZE` bleibt aus). - Content länger als `MAX_MESSAGE_LENGTH` → `success=False` zurückgeben statt still zu kürzen, damit der Aufrufer auf `send()` (das splittet) zurückfällt. - Fehler von chattolib → `success=False, retryable=True`. - Editierte ID via `_mark_seen()`, damit das eigene `message_edited`-Event nicht zurückläuft. ## 2. `delete_message` — base.py:4064, Default `False` chattolib: `client.delete_message(room_id, event_id) -> bool`. Genutzt vom Fresh-Final-Cleanup des Stream-Consumers und von `_schedule_ephemeral_delete` (EphemeralReply-TTL). Durchreichen, Exceptions zu `False`. ## 3. Eingehende Attachments → `MessageEvent.media_urls` / `media_types` Kein Override, sondern eine Lücke: `_dispatch_message_posted` verwirft Nachrichten mit leerem `body` — ein reines Bild/PDF an Hermes verschwindet. - `Message.attachments` (`list[MessageAttachment]`) trägt `asset_url.url` (vorsignierte URL), `filename`, `content_type` — kein `get_asset`-Roundtrip nötig. - Bytes laden (httpx, vendored), gegen `validate_inbound_media_size` prüfen, durch `cache_media_bytes(data, filename=…, mime_type=…)` schicken — das ist der geteilte Funnel aller Adapter (base.py:2212). - `media_urls` = lokale Cache-Pfade, `media_types` = MIME, `message_type` nach derselben Präzedenz wie Teams/Signal: DOCUMENT > PHOTO > VIDEO > AUDIO > TEXT. - Early-Return nur noch, wenn *weder* Body *noch* Attachments da sind. ## 4. `create_handoff_thread` — base.py:4008, Default `None` Seed-Message in den Parent-Room posten, deren ID ist der Thread-Root (dasselbe Muster wie Slack, adapter.py:2278 dort). Best-effort `follow_thread`. Bei DM-Räumen `None` (Chatto-DMs können keine Threads). ## 5. `send_document` / `send_video` / `send_voice` — base.py:4718 / 4691 / 4545 Defaults schicken „⚠️ Couldn't deliver …" als Text. Die Upload-Mechanik gibt es schon (`_upload_asset`, chunked), sie hängt nur an `send_image_file`. - Gemeinsamen Helper `_send_local_attachment()` aus `send_image_file` herausziehen; die vier Methoden werden dünne Wrapper. - `validate_media_delivery_path()` bleibt der Gate vor jedem Upload. - Fällt der Upload aus, greift weiter der Textkommentar der Basis-Semantik (nie den Host-Pfad in den Chat schreiben). ## 6. `send_multiple_images` — base.py:4398 Default sendet einzeln. Chatto-Nachrichten tragen mehrere Attachments: alle hochladen, *ein* `post_message` mit `attachment_asset_ids=[…]`. Bei ≤1 Bild oder Upload-Fehler auf `super()` zurückfallen. `file://`-URIs entquoten, `http(s)://` vorher herunterladen. ## 7. Reaction-Events → `_reaction_handler` (`set_reaction_handler`, base.py:3691) `reaction_added`/`reaction_removed` werden in `_handle_realtime_event` heute explizit verworfen. `ReactionPayload` (`room_id`, `message_event_id`, `emoji`) ist fertig dekodiert. - Normalisiertes Dict exakt im Slack-Format: `platform`, `event_name` (`reaction:added` / `reaction:removed`), `reaction`, `user_id`, `item_user_id`, `item_type`, `channel_id`, `message_ts`, `event_ts`, `raw_event`. - Eigene Reaktionen (Lifecycle 👀/✅/❌) rausfiltern, sonst Endlosschleife. - Handler-Aufruf non-blocking (try/except), `getattr`-Guard wie bei Slack. - Nebenbei: `message_edited`/`message_retracted` in die Known-Kinds-Liste, damit sie nicht als `unknown event kind` geloggt werden. ## 8. `format_message` — base.py:7235, Default = Identität Bewusst minimal: Chatto rendert Markdown nativ, es gibt nichts zu escapen. Implementiert wird nur, was messbar schiefgeht — CRLF-Normalisierung und das Kappen von >2 Leerzeilen am Stück. Zusätzlich der `hasattr`-Aufruf in `send()` (adapter.py:681) durch einen direkten Aufruf ersetzt, der heute effektiv ein No-op ist. **Dünnster Punkt der Liste** — mehr wäre Spekulation über Chatto-Renderer-Details, die sich real-testen lässt. --- ## Tests Alle in `test_adapter.py`, gleicher Stil (AsyncMock-Client, keine Netzwerk-IO): - `TestMessageEditing`: die beiden `xfail(strict=True)`-Marker entfernen, ergänzen: Overlong-Content → `success=False`, chattolib-Fehler → `retryable`. - `TestInboundAttachments`: Bild-Attachment → `media_urls`/`media_types` gesetzt, `message_type == PHOTO`; Body leer + Attachment → nicht verworfen; Body leer + kein Attachment → weiterhin verworfen; Download-Fehler → Event trotzdem zugestellt. - `TestHandoffThread`: Room → Seed-ID; DM → `None`. - `TestNativeSends`: `send_document`/`send_video`/`send_voice` laden hoch und posten mit `attachment_asset_ids`; unsicherer Pfad → Fallback-Text. - `TestSendMultipleImages`: zwei lokale Dateien → *ein* `post_message` mit zwei Asset-IDs. - `TestReactionForwarding`: Fremd-Reaktion → Handler mit korrektem Dict; Selbst-Reaktion → kein Handler-Aufruf; Handler-Exception → kein Absturz. - `TestFormatMessage`: CRLF und Leerzeilen. Lauf: `HERMES_ROOT=/tmp/hermes uv run --group dev --with pyyaml pytest -q` (das Plugin hängt nicht am Agent; `HERMES_ROOT` zeigt auf ein Checkout). ## Nicht in Scope `send_draft`/`supports_draft_streaming` (Telegram-Draft-API, kein Chatto-Pendant), Streaming-TTS, `enforces_own_access_policy` (Security-Entscheidung, gehört separat besprochen — siehe Punkt 9 der Analyse).