# Plan: Eingehende Edits verarbeiten (feat/edit-dispatch)

Referenz: `gateway/platforms/base.py` aus dem Hermes-Checkout; chattolib
0.4.20.post1 (`MessageEditedPayload`, realtime protocol v1).

Ein `message_edited`-Event wird je nach Zustand der Original-Nachricht zu
einer Korrektur, einem Nachtrag oder nichts. Drei Pfade:

1. **Mid-run-Korrektur** – Nachricht wird gerade verarbeitet: Lauf canceln
   (Muster `/stop`, `cancel_session_processing`), mit korrigiertem Text neu
   dispatchen. 🚫 entsteht über den CANCELLED-Outcome-Hook (base.py:6345),
   👀 wieder über den Neustart.
2. **Nachtrag** – Nachricht wurde nie dispatcht (z. B. Mention nachgetragen):
   Admission-Gates gegen den neuen Body, dann normaler Turn.
3. **Schon beantwortet** – ignorieren. `_dispatched_ids` ist die Sperre gegen
   die Retrospektive-Maschine.

Konsistenzregel: Gates gelten in beide Richtungen — wer die Mention beim
Berichtigen herauseditiert, fällt durchs Gate und wird ignoriert.

---

## Konfiguration (Drei-Namen-Schema)

- `CHATTO_EDIT_DISPATCH` (bool, **default true**) — Master-Schalter.
  Default an: das Verhalten ist Teil des README-Versprechens.
- `CHATTO_EDIT_WINDOW` (int Sekunden, default 300) — max. Alter eines Edits
  via `Message.updated_at − created_at` (types.py:673/678). Neuer "int"-Kind
  in der ConfigField-Maschinerie (`_get_env_or_extra_int`); unparsebare
  Werte fallen mit Warnung aufs Default.
- plugin.yaml: beide als optional_env mit `(default: …)`-Angabe.

## Adapter

- `_dispatched_ids`: Ring-Puffer (SEEN_CAP-Muster) — gefüllt vor jedem
  `handle_message`, both paths. Retro-Sperre.
- `_processing: dict[session_key, message_id]` — geschrieben in
  `on_processing_start`, geräumt in `on_processing_complete` (feuert bei
  Cancellation mit CANCELLED, base.py:6345-6350). Liefert Pfadwahl UND den
  Cancel-Schlüssel.
- `_session_key_for(source)`: ruft `gateway.session.build_session_key` mit
  exakt denselben Extras wie `handle_message` (base.py:5578) — ohne das
  trifft `cancel_session_processing` die falsche Session.
- `_admit_and_build(...)`: die Admission-Pipeline aus `_dispatch_message_posted`
  ausgezogen (User-Cache → Auth → Room-Kind → DM-Commands → Mention-Gates →
  🫥-Ack → Thread-Anker → Source/Event → Media). Beide Pfade teilen sie;
  Flag `allow_dm_commands=False` für Edits — ein editiertes `/join` läuft
  weder doppelt noch zum Agenten durch.
- `_dispatch_message_edited(payload)`: Echo-Filter (actor == me, im Event-Zweig)
  → Respond-Room-Gate → Hydration (`deleted_at` skip) → Fenster → **billige
  Retro-Triage vor der Pipeline** (schon erledigt ⇒ keine API-Kosten, kein
  zweites 🫥) → `_admit_and_build` → Pfadentscheidung:
  - inflight ⇒ `cancel_session_processing(discard_pending=False)` — geparkte
    Folgenachrichten überleben; der neue Turn drainiert sie am Ende selbst.
  - queued (`_pending_messages[session_key].message_id == id`) ⇒ Text in
    place umschreiben, kein zweiter Dispatch.
  - sonst nie dispatcht ⇒ frischer Turn.
- Einbau in `_handle_realtime_event` als eigener Zweig; `message_edited`
  fliegt aus der Known-Kinds-Liste.

## Tests

`test_adapter.py::TestEditDispatch` (14 Fälle): Default on + Fenster 300 ·
Echo ignoriert · Feature aus · Read-only-Room ohne API-Call · Mention
nachgetragen ⇒ Turn · weiter ohne Mention ⇒ still · schon beantwortet ⇒
ignoriert · gelöscht ⇒ ignoriert · Edit außerhalb des Fensters ⇒ ignoriert ·
frischer Edit in DM ⇒ Turn · Mid-run ⇒ Cancel + Redispatch mit neuem Text ·
Completion-Hook räumt `_processing` ab (Re-Arming) · queued Follow-up wird
in place korrigiert (kein Cancel, kein zweiter Dispatch) · DM-Command-Edit
läuft weder Command noch Agent.

`test_platform_config.py`: `_get_env_or_extra_int` (Env-Präferenz, Extra int
und str, Junk ⇒ Default, YAML-bool ⇒ Default, beides fehlt ⇒ Default).

Lauf: `PYTHONPATH=/path/to/hermes-agent uv run --with pyyaml pytest -q`

## Doku

README.md: Verhaltenstabelle (neue Zeile „Editing your messages"),
Capability-Matrix („inbound edit processing"), Env-Beispiele, Env-Tabelle,
config.yaml-Beispiel, Usage Note „Editing Your Messages" mit Pflichtenheft-
Tabelle. AGENTS.md: Behavioural-Contract-Bullet. after-install.md: Satz im
Test-Schritt (First-run-Verhalten).

## Nicht in Scope

Anhänge im Edit berücksichtigen (queued rewrite toucht nur Text; Media bleibt
vom Original). Mehrere schnelle Edits debouncen — `fetch_message` liefert
ohnehin den neuesten Stand. Retraction (`message_retracted`) verarbeiten.
Edits an Nachrichten in anderen Sessions gezielt in deren Queues korrigieren
(der Pending-Match läuft über die Session-Key-Gleichheit).
