PLAN.md 5.9 KB

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_LENGTHsuccess=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).