Sitzung und Zustellung
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
class SessionSource:149-299— Datenklasse der Nachrichtenquelle (platform/chat/sender, mit Hash)class SessionContext:299-700— Sitzungskontext (Key, Quelle, Fortsetzungsstrategie)ID-Hash:40-74—_hash_id/_hash_sender_id/_hash_chat_id(Privatsphäre + Stabilität)Pfad-Sicherheitsprüfung:100-149—_is_path_unsafe/_is_session_key_unsafe(Session-Key fließt in Dateisystem-Pfade)auto_continue Frische-Fenster:26-40—auto_continue_freshness_windowDelivery-Ledger-Doku:1-58— Designziel: best-effort, Fehler blockieren nie den Hauptfluss (Ledger (ledger) = Register/Buch)compute_obligation_id:146-155— eindeutige Verbindlichkeits-ID (Session-Key + Nachrichten-Ref + Inhalt)Zustandsautomat:155-203—record_obligation/mark_attempting/mark_delivered/mark_failedsweep_recoverable:203-276— scannt nach Crash recoverable Verbindlichkeiten und sendet nachSQLite-Verbindung:77-102—_db_path/_connect, persistiert in lokalem SQLite
Datenfluss
- Der Adapter (adapter) empfängt ein Plattform-Event und extrahiert über
MessageEvent:1759dieSessionSource. SessionSource:149hasht eine stabile Chat-/Sender-ID (gateway/session.py:40).- Konstruktion des
SessionContext:299, ergibt den Session-Key (nachPfad-Sicherheitsprüfung:109als Platten-Pfad genutzt). - Der Session-Key bestimmt, aus welcher Datei die Historie geladen und dem Agenten zum Kontextfortsetzen gefüttert wird.
- Sobald der Agent die finale Antwort erzeugt hat, verbucht das Gateway in
record_obligation:155→ senden →mark_delivered. - Wenn beim Senden ein Crash passiert, scannt beim nächsten Start
sweep_recoverable(gateway/delivery_ledger.py:203) Verbindlichkeiten im Statusattemptingmit totem Besitzer-Prozess und sendet nach oder markiert als failed.
SessionSource hasht sensible Felder wie Plattform + Chat + Sender; direkt sha256, 12 Zeichen lang:
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:
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:
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:
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_unsafeweist/strikt ab,_is_session_key_unsafenur..und führendes/. Google Chatsspaces/<id>ist ein legalersession_key, aber keinsession_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ßerdemneeds_marker(weil das ursprünglichesendhalb erfolgreich gewesen sein könnte). - Plattform nicht verbunden:
deliverable_platformsfiltert Plattformen, die in diesem Boot offline sind, und verhindert, dass jeder Neustart einen Versuch verbrennt, bisMAX_ATTEMPTSerreicht ist und das Rowabandonedwird. Erst ein Boot mit Verbindung liefert nach. - Sitzung abgelaufen:
auto_continue_freshness_windowist 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.