# 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).
