Skip to content

Sitzung und Zustellung

源码版本v2026.7.20

Verantwortung

Zwei Dinge: (1) Sitzung (session) – «woher die Nachricht kommt» (Plattform, Chat, Sender) wird auf einen stabilen Key abgebildet und der Dialog auf der Platte persistiert, sodass der Agent über Neustarts hinweg Kontext (context) fortsetzt; (2) Delivery Ledger (delivery ledger) – für die finale Antwort des Agenten wird eine «Zustellverbindlichkeit» verbucht; nach einem Crash kann das Gateway (gateway) unzustellbare Antworten erkennen und nachsenden, sodass der Nutzer sie letztlich erhält. Beide liegen im Gateway und sind vom Agenten entkoppelt.

Designmotiv

Der Agent selbst ist zustandslos — er dreht nur eine Runde Schleife; woher der Kontext kommt und wohin die finale Antwort geht, ist nicht seine Sache. Die Sitzungsschicht löst «Nachrichten derselben Person im selben Chat zum selben Thema gehören zu demselben Dialog»; diese Schicht muss den stabilen Key auf Platte legen, sonst kennt der Agent nach einem Neustart den vorherigen Text nicht. Das Delivery-Ledger löst «der Agent hat die Antwort generiert, aber das Gateway ist mitten in send() gecrasht» — eine derart halb gestorbene Antwort sieht der User weder vollständig, noch lässt sie sich leicht durch erneutes Generieren duplizieren. Das Ledger bucht jede «zustellbare» Verbindlichkeit in SQLite; nach dem Neustart genügt ein Sweep, um nachzusenden oder als gescheitert zu markieren, statt wegzuwerfen und neu anzufangen.

Schlüsseldateien

Datenfluss

  1. Der Adapter (adapter) empfängt ein Plattform-Event und extrahiert über MessageEvent:1759 die SessionSource.
  2. SessionSource:149 hasht eine stabile Chat-/Sender-ID (gateway/session.py:40).
  3. Konstruktion des SessionContext:299, ergibt den Session-Key (nach Pfad-Sicherheitsprüfung:109 als Platten-Pfad genutzt).
  4. Der Session-Key bestimmt, aus welcher Datei die Historie geladen und dem Agenten zum Kontextfortsetzen gefüttert wird.
  5. Sobald der Agent die finale Antwort erzeugt hat, verbucht das Gateway in record_obligation:155 → senden → mark_delivered.
  6. Wenn beim Senden ein Crash passiert, scannt beim nächsten Start sweep_recoverable (gateway/delivery_ledger.py:203) Verbindlichkeiten im Status attempting mit totem Besitzer-Prozess und sendet nach oder markiert als failed.

SessionSource hasht sensible Felder wie Plattform + Chat + Sender; direkt sha256, 12 Zeichen lang:

python
def _hash_id(value: str) -> str:
    """Deterministic 12-char hex hash of an identifier."""
    return hashlib.sha256(value.encode("utf-8")).hexdigest()[:12]

def _hash_sender_id(value: str) -> str:
    """Hash a sender ID to ``user_<12hex>``."""
    return f"user_{_hash_id(value)}"

Hash erstens für die Privatsphäre (plattformseitige user_id nicht direkt auf Platte), zweitens für Stabilität — nach einem Gerätewechsel derselbe Mensch setzt in derselben Session fort. Danach fließt der Session-Key in einen Dateisystem-Pfad, also gibt es eine zweite Prüfung:

python
def _is_path_unsafe(value: object) -> bool:
    """Return True if ``value`` could traverse outside the sessions dir."""
    if not value:
        return False
    s = str(value)
    if ".." in s or "/" in s or "\\" in s:
        return True
    # Leading Windows drive path, e.g. "C:\\..." or "d:/...".
    return len(s) >= 2 and s[0].isalpha() and s[1] == ":"

def _is_session_key_unsafe(value: object) -> bool:
    """True if ``value`` could be a real traversal vector in a session_key.
    ``session_key`` is a *logical* routing key (e.g.
    ``agent:main:google_chat:group:spaces/<id>``) — it never touches the
    filesystem, so the strict separator-rejecting guard from
    ``_is_path_unsafe`` is over-broad"""

Die beiden Prüffunktionen sind bewusst verschieden — session_key ist ein logischer Routing-Key (Google Chats spaces/<id>/threads/<id> enthält von Natur aus /); striktes Abweisen von / würde fälschlich killen. Aber Felder wie session_id, die in einen Dateipfad fließen, müssen strikt abweisen werden. Diese Trennung vermeidet die Kompatibilitätsfalle «eine Prüfung für alles».

Der Zustandsautomat des Delivery-Ledgers besteht aus vier Methoden + einer SQLite-Tabelle:

python
def compute_obligation_id(session_key: str, message_ref: str, content: str) -> str:
    """Stable id: same turn + same content re-records idempotently, while
    distinct threads/topics on the same chat can never collide."""
    payload = f"{session_key}|{message_ref}|{content}"
    return hashlib.sha256(payload.encode("utf-8", "replace")).hexdigest()[:24]

def record_obligation(*, obligation_id, session_key, platform, chat_id, thread_id, content) -> None:
    """Record a final response as owed to the platform (state='pending')."""

obligation_id hasht session_key + message_ref + content zu 24 Zeichen Hex — dieselbe Runde + derselbe Inhalt ist idempotent (ein wiederholtes Generieren bläht das Ledger nicht auf); verschiedene Threads im selben Chat kollidieren niemals.

Crash-Recovery über sweep_recoverable ist der Kern des Designs:

python
def sweep_recoverable(now=None, *, deliverable_platforms=None) -> List[Dict[str, Any]]:
    """Claim undelivered rows owned by dead processes; return them for
    redelivery.

    Claiming atomically re-stamps the owner to THIS process and increments
    ``attempts``, so a second gateway racing the same sweep cannot
    double-claim (the UPDATE is guarded on the previous owner stamp).
    Rows over the attempts cap or older than the stale cutoff transition to
    'abandoned' instead of being returned.
    """

Entscheidend ist das «Claim» — erreichen zwei Gateway-Instanzen dasselbe Row, gewinnt, wer zuerst owner_pid auf sich selbst umstempelt; die WHERE-Klausel des UPDATE stellt sicher, dass nur einer erfolgreich ist. deliverable_platforms verhindert «umsonst Versuche verbrennen» — Plattformen, die dieser Boot nicht verbunden hat, werden nicht beansprucht, sondern einem künftigen Boot überlassen, das zustellen kann; sie werden nicht bei jedem Neustart näher an die MAX_ATTEMPTS-Obergrenze geschoben.

Grenzen und Fehler

  • Pfad und logischer Key verwechselt: _is_path_unsafe weist / strikt ab, _is_session_key_unsafe nur .. und führendes /. Google Chats spaces/<id> ist ein legaler session_key, aber kein session_id — wer hier verwechselt, killt entweder fälschlich oder erzeugt Pfad-Traversal.
  • Doppelte Zustellung: Erreichen zwei Gateways dasselbe Row, muss das Claim atomar sein (UPDATE WHERE auf altem owner_pid), sonst wird dieselbe Antwort zweimal gesendet. Nachgesendete Rows tragen außerdem needs_marker (weil das ursprüngliche send halb erfolgreich gewesen sein könnte).
  • Plattform nicht verbunden: deliverable_platforms filtert Plattformen, die in diesem Boot offline sind, und verhindert, dass jeder Neustart einen Versuch verbrennt, bis MAX_ATTEMPTS erreicht ist und das Row abandoned wird. Erst ein Boot mit Verbindung liefert nach.
  • Sitzung abgelaufen: auto_continue_freshness_window ist das Zeitfenster für «an den vorherigen Abschnitt anknüpfen»; nach Ablauf wird nicht mehr auto-continued, damit eine Stunden alte Sitzung nicht als aktuelle Fortsetzung gilt.

Zusammenfassung

Die Sitzungsschicht ist zuständig für «zu welchem andauernden Dialog gehört diese Nachricht» und bildet die externe Identität per Hash + Pfad-Prüfung sicher auf Plattendateien ab. Das Delivery-Ledger ist zuständig für «ob die finale Antwort des Agenten wirklich beim Nutzer ankam» und macht über einen SQLite-Zustandsautomaten Crash-Recovery. Beides ist best-effort, aber eine unverzichtbare Zuverlässigkeitsschicht.

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.