Blog
Gesprächsverlauf gehört auf den Server: Wie Tactic Remote Sitzungen auf Mobilgeräten wiederherstellt
Warum Tactic Remote 1.8.2 den Chatverlauf vom Mobilgerät auf den Server verlagert hat – und wie diese Entscheidung eine ganze Klasse von Problemen mit Nachrichtenreihenfolge und Historieverlust strukturell beseitigt.
Weitere Produkt- und Engineering-Updates aus dieser Kategorie.
Kategorie öffnenDieser Artikel beschreibt eine Architekturentscheidung: warum Tactic Remote 1.8.2 den Chatverlauf vom Mobilgerät auf den Server verlagert hat – und wie diese Entscheidung dazu geführt hat, dass eine Reihe von Problemen, an denen wir immer wieder geflickt haben, strukturell verschwunden ist.
Ein Ansatz, der zum Scheitern verurteilt war
Vor Version 1.8.2 wurde der Chatverlauf vollständig auf dem Mobilgerät in Echtzeit zusammengesetzt. Der Server streamt die Ereignisse des Agenten an das Gerät, das Gerät rendert sie der Reihe nach als Sprechblasen und baut so die sichtbare Unterhaltung auf.
Am Computer funktioniert das noch halbwegs, weil die Verbindung stabil ist. Auf Mobilgeräten hat dieser Ansatz jedoch einen grundlegenden Konstruktionsfehler: Die Historie existiert nur innerhalb der aktuellen Verbindung und der aktuellen Sitzung. Die Folgeprobleme ließen nicht lange auf sich warten.
- Nach einem Reconnect bekommt die neue Verbindung keine alten Ereignisse mehr – die Historie ist leer oder unvollständig.
- Wird die App beendet und neu gestartet, ist die Unterhaltung aus dem Arbeitsspeicher verschwunden; man fängt von vorne an.
- Verbinden sich mehrere Geräte mit derselben Sitzung, sieht jedes nur die Ereignisse ab dem eigenen Verbindungszeitpunkt – alle reden aneinander vorbei.
Hinzu kommt ein subtileres Problem mit der Nachrichtenreihenfolge. Beim Absenden einer Nachricht zeigt das Gerät die Benutzer-Sprechblase sofort optimistisch an, während die erste Antwort des Agenten erst eintrifft, nachdem sein Prozess tatsächlich gestartet ist. Jedes kleinste Timing-Jitter – etwa eine verzögert eintreffende Nachricht nach einem Reconnect – kann dazu führen, dass die Benutzer-Sprechblase unterhalb der Antwort landet. Dieses „Sprechblase-rutscht-nach-unten"-Problem haben wir mehrfach gepatcht, aber jedes Mal nur an der Oberfläche. Der eigentliche Grund ist struktureller Natur: Im gesamten System war niemand dafür verantwortlich, die tatsächliche Reihenfolge der Unterhaltung zu definieren.
Das Konzept von tmux übernehmen
Der Terminalmodus von Tactic Remote ist stabil – dank tmux. Das tmux-Modell ist einfach und wirkungsvoll: Der Terminal-Puffer ist die einzige Quelle der Wahrheit. Wer sich verbindet (attach), bekommt diesen Puffer zurückgespielt. Verbindungsabbrüche, Gerätewechsel, Neustarts – all das bedeutet nur, denselben Puffer erneut abzurufen; die Historie ist selbstverständlich noch vorhanden.
Genau diese Schicht fehlte im Chatmodus. Die Idee hinter dem Design von 1.8.2 war daher, dieses Konzept zu übertragen:
Der Server führt eine maßgebliche Gesprächsaufzeichnung; beim Verbinden stellt der Client sie vollständig wieder her.
Sobald der Server die Autorität über die Reihenfolge übernimmt, sind die oben beschriebenen Probleme keine einzeln zu behebenden Bugs mehr – sie können strukturell gar nicht mehr auftreten.
Wie es im Detail funktioniert
Serverseitig: ein Ringpuffer pro Sitzung
Der Server führt für jede Chatsitzung eine Gesprächsaufzeichnung. Jedes ausgesendete Ereignis wird der Reihe nach in einen Ringpuffer geschrieben. Der Puffer hat zwei Obergrenzen: eine für die Anzahl der Ereignisse und eine für die Gesamtbytezahl; wird eine davon überschritten, werden die ältesten Einträge entfernt. Das Initialisierungsereignis zu Beginn einer Sitzung – der Ankerpunkt für die Wiederherstellung – wird jedoch niemals entfernt. Sobald Einträge entfernt wurden, erhält der Puffer einen „gekürzt"-Vermerk, damit der Client beim Wiederherstellen oben deutlich anzeigen kann „Ältere Nachrichten wurden ausgelassen" – anstatt eine vollständige Historie vorzutäuschen.
Ein entscheidender Schritt: Nachrichten des Benutzers erzeugen nun ebenfalls ein geordnetes Ereignis auf dem Server, das in die Aufzeichnung geschrieben wird. Früher existierten Benutzernachrichten nur im optimistischen Rendering auf dem Gerät; der Server wusste nichts davon. Steuerungsnachrichten (steer), die mitten in einer laufenden Sitzung eingefügt wurden, waren überhaupt nicht erfasst. Da der Server jetzt die Autorität über die Reihenfolge besitzt, landet jede Benutzeräußerung genau dort, wo sie tatsächlich stattgefunden hat. Das alte „Sprechblase-rutscht-nach-unten"-Problem existiert strukturell nicht mehr.
Wiederherstellung: ein Umschlag statt erneutes Senden der Rohereignisse
Der ursprüngliche Ansatz war, beim Verbinden die Rohereignisse einfach nochmals zu senden. Diese Idee wurde schnell verworfen. Ältere Clients ignorieren zwar unbekannte Ereignisse, würden aber die erneut gesendeten – bekannten – Ereignisse als neue Echtzeitereignisse auffassen und sie ein weiteres Mal anhängen; die Historie würde sich verdoppeln.
Der gewählte Ansatz besteht darin, die gesamte Aufzeichnung in ein neues Ereignis zu verpacken und auszuliefern: structured_replay, das Sequenznummer, Flag für letztes Paket, Kürzungs-Flag, aktuellen Laufstatus sowie die Ereignisliste enthält. Ältere Clients kennen dieses Ereignis nicht und ignorieren es stillschweigend – kein Schaden entsteht. Neuere Clients packen den „Umschlag" aus und speisen die enthaltenen Ereignisse in denselben Verarbeitungspfad ein, um die gesamte Unterhaltung neu aufzubauen.
Da iOS WebSocket-Frames auf etwa 1 MiB begrenzt, werden Wiederherstellungsdaten in Blöcken von 256 KB ausgeliefert und mit Sequenznummern versehen. Bereits bearbeitete Tool-Freigaben werden bei der Wiederherstellung mit einem „genehmigt / abgelehnt"-Vermerk versehen, damit eine historische Freigabekarte nicht fälschlicherweise als offene, auf Eingabe wartende Karte dargestellt wird.
Clientseitig: beim Verbinden wiederherstellen, Seiteneffekte unterdrücken
Empfängt der Client das erste Wiederherstellungspaket (Sequenznummer 0), leert er zunächst den lokalen Gesprächsverlauf und speist dann die Ereignisliste der Reihe nach in dieselbe Verarbeitungsfunktion ein. Wiederherstellung und Echtzeit nutzen also exakt denselben Renderpfad – keine separate Logik erforderlich.
Ein Detail, das zwingend berücksichtigt werden musste: Seiteneffekte, die ausschließlich im Echtzeitbetrieb ausgelöst werden sollen, müssen während der Wiederherstellung unterdrückt werden – etwa automatisches Absenden aus der Warteschlange, Timeout-Watchdogs, Zustandswechsel des Laufstatus und automatische Freigabe-Bereitschaft. Ein einziges Flag hält all das zurück, bis das letzte Paket eingetroffen ist; erst dann wird der maßgebliche Laufstatus aus dem Umschlag übernommen.
Die optimistisch angezeigte Benutzer-Sprechblase beim Senden haben wir beibehalten. Da der Agentenprozess ein bis zwei Sekunden braucht, um zu starten, würde reines Warten auf die Wiederherstellung eine spürbare Lücke hinterlassen. Stattdessen wird die Sprechblase als „ausstehend"-Platzhalter behandelt: Sobald die Serverbestätigung eintrifft, wird der Platzhalter entfernt und die Nachricht an ihrer tatsächlichen Position in der maßgeblichen Aufzeichnung neu eingefügt. Reihenfolge-Chaos kann so strukturell nicht fortbestehen.
Kompatibilität: warum „unbekannte Ereignisse ignorieren" eine Designentscheidung ist
Dass die Wiederherstellungslösung sicher eingeführt werden konnte, verdankt sich in erheblichem Maß einer früh getroffenen Vereinbarung: Ereignisse, die ein Client nicht kennt, werden stillschweigend ignoriert. Genau das ermöglicht es dem Server, ein neues Ereignis wie structured_replay einzuführen, ohne dass noch nicht aktualisierte ältere Clients davon betroffen wären. Sie sehen die Wiederherstellungsfunktion schlicht nicht – aber sie werden auch nicht durch sie gestört.
In einem System, in dem iOS-App, Server für Mac / Linux / Windows und ein npm-CLI unabhängig voneinander veröffentlicht werden und die Versionen dauerhaft auseinanderdriften, ist diese Protokolldisziplin – nur Ergänzungen, keine Änderungen, Unbekanntes ignorieren – die Voraussetzung dafür, dass der Server voranschreiten und der Client nachziehen kann.
Grenzen – und ehrliche Abwägungen
Dieses Design hat Grenzen; wir ziehen es vor, sie offen zu kommunizieren.
- Der Serverpuffer hat eine Obergrenze. Bei sehr langen Sitzungen werden die frühesten Nachrichten entfernt. Beim Wiederherstellen wird das mit „Ältere Nachrichten wurden ausgelassen" klar kenntlich gemacht – keine vorgetäuschte Vollständigkeit.
- Vom Server freigegebene Sitzungen beginnen von vorne. Bleibt eine Sitzung lange ohne Subscriber, gibt der Server sie frei; eine danach per
claude --resumewieder aufgenommene Sitzung startet mit einem leeren Puffer, ältere Ereignisse werden nicht zurückgespielt. Das ist eine bewusste Abwägung: begrenzte Historiespeicherung gegen kontrollierten Speicherverbrauch. - Lokale Persistenz auf dem Client ist ein späterer Schritt. Die maßgebliche Aufzeichnung existiert derzeit nur, solange der Server läuft. Sie dauerhaft auf dem Client zu speichern, ist eine geplante Erweiterung – aber nicht Teil dieser Version.
Lieber kommunizieren wir diese Grenzen im Voraus, als dass Sie eines Tages feststellen, dass ein Teil der Historie fehlt, und sich selbst einen Reim darauf machen müssen.
Möchten Sie das „beim Verbinden sofort wiederhergestellt"-Erlebnis selbst ausprobieren? Die iOS-App ist kostenlos im App Store erhältlich, Begleit-App und CLI sind ebenfalls kostenlos. Wenn Sie das andere harte Problem in dieser Version interessiert – ein Einfrieren der Benutzeroberfläche, das nichts mit dem Business-Code zu tun hatte – lesen Sie „Auf der Spur eines eingefrorenen Interfaces".
Tactic Remote ausprobieren
Coding-Agenten vom Smartphone aus steuern
Verbinde dich mit Claude Code, Codex und anderen Agenten auf deinem Mac-, Windows- oder Linux-Rechner. Prüfe den Fortschritt und sende die nächste Anweisung vom iPhone oder iPad.