# Android: Projektionen, Batch-Abgleich und Verwahrung

> Wie die Android-App den Agent wählt, Agent-Limits liest und Konten wechselt, Timeline-Projektionen zwischenspeichert, Timelines in Batches abgleicht, jede Sendung verschlüsselt verwahrt, einen Zustellmodus wählt und Nachrichten zwischen Sessions zeigt.

Handbuch / Android-App · Gegen den Quellcode geprüft am 2026-10-05 · https://www.hbcode.ai/de/docs/android/chat

## Sie wählen den Agenten, wenn Sie die Session erstellen

Der Dialog für eine neue Session zeigt eine Agent-Auswahl mit Codex und Claude. Der Agent steht für die Lebensdauer der Session fest, deshalb wählen Sie ihn vor session.create, und die Anfrage trägt ihn. NewSessionAgentPreference hält Ihre letzte Wahl in SharedPreferences. Vor der ersten Wahl steht die Auswahl auf Codex.

Der Dialog kann auch eine gespeicherte Unterhaltung des gewählten Agents im selben Projektordner fortsetzen. Die Unterhaltungsauswahl lädt sie über run.chatExistingSessions und zeigt eine Vorschau über run.chatPreviewExistingSession, und session.create trägt dann ihre conversationId.

Danach durchläuft eine Claude-Session dieselben Projektionen, Batches und dieselbe Verwahrung wie eine Codex-Session, mit denselben Zeilen und denselben run.chat*-Methoden. Die Antwort auf das Laden des neuesten Fensters und jede abgeglichene Session haben eine geschlossene Feldmenge, zu der nativeSessionId, threadId, boundaryToken und storageFormat gehören. Der Decoder akzeptiert für storageFormat native oder indexed und lehnt alles andere ab.

Der Composer ist der Teil, der sich je Agent unterscheidet.

| Composer-Regel | Codex-Session | Claude-Session |
| --- | --- | --- |
| Die letzte gesendete Nachricht ersetzen | Wird angeboten, wenn kein Run aktiv und der Entwurf leer ist. | Wird nicht angeboten. Ein Senden in diesem Zustand wird lokal abgelehnt. |
| Geschwindigkeitsmodus einer Nachricht | Standard oder Fast. | Immer Standard. |
| Zugriffsmodi, in Listenreihenfolge | Full access, Full auto, Never ask, Read only. | Plan mode, Accept edits, Auto mode, Bypass permissions. |
| Hervorgehobene Modellfamilien | astra, sol, terra, luna. | fable, opus, sonnet, haiku. |

Beide Listen der Zugriffsmodi senden dieselben vier Wire-Werte: read-only, full-auto, full-access und unrestricted. Die Claude-Liste zeigt sie in dieser Reihenfolge unter den Namen von Claude. Eine Session ohne gespeicherten Zugriffsmodus läuft in full-auto.

- [Wahl des Agenten](https://www.hbcode.ai/de/docs/agents.md#agent-choice): Der Desktop-Vertrag für den Agent einer Session.
- [Zugriffsstufen](https://www.hbcode.ai/de/docs/agents.md#access-levels): Was jede der vier Stufen bei jedem Agent erlaubt.

## Agent-Limits und Kontowechsel

**Nach einem Codex-Wechsel fallen die alten Limits weg, und die Kachel zeigt „Checking“ bis zum nächsten Lesen**

Drei Spuren, nicht maßstäblich. Der Projects-Bildschirm liest die Agent-Konten des ausgewählten Desktops beim Öffnen und dann alle 60 Sekunden. Sie tippen auf ein anderes Codex-Abonnement. Der Desktop wechselt und antwortet mit den Profilen. Das Telefon setzt die Limits auf unbekannt, sodass die Kachel „Checking“ zeigt, und liest die Limits erneut. Später wählen Sie einen anderen Desktop, während ein Lesevorgang unterwegs ist. Seine Antwort gilt dem alten Desktop und wird verworfen, und die Kachel beginnt wieder mit „Checking“.

- **Geplantes Lesen** (system.codexUsageLimits · system.codexAuthProfiles · system.claudeAccounts): Der Projects-Bildschirm liest beim Öffnen, wenn nichts geladen ist, und dann alle 60 Sekunden. Die Einstellungen lesen einmal für einen Agent, den sie noch nie gesehen haben. Limits oder Profile allein gelten trotzdem, zusammen mit dem Fehler des anderen Aufrufs.
- **Codex-Wechsel** (system.activateCodexAuthProfile): Schlüssel: system.activateCodexAuthProfile:<profileId>:<uuid>. Das Telefon sendet nichts, wenn das Profil nicht angemeldet ist, schon aktiv ist oder ein anderer Wechsel läuft. Die Antwort enthält die Profile. Die Limits gehören zum alten Abonnement, deshalb liest das Telefon sie erneut.
- **Claude-Wechsel** (system.activateClaudeAccount): Schlüssel: system.activateClaudeAccount:<accountId>:<uuid>. Die Antwort enthält jedes Konto mit seinen Nutzungsfenstern, deshalb folgt kein zweites Lesen. Der Decoder verlangt eindeutige Konto-IDs und genau ein aktives Konto.
- **Desktop-Schranke** (isCurrentDesktopSelection): Jede Anfrage merkt sich den ausgewählten Desktop. Eine Antwort, die ankommt, nachdem Sie einen anderen Desktop gewählt haben, wird verworfen. Die Wahl eines Desktops leert außerdem beide Kontozustände.

AgentAccountsCoordinator liest die Agent-Konten des ausgewählten Desktops. Für Codex sendet er system.codexUsageLimits und system.codexAuthProfiles, für Claude einen Aufruf system.claudeAccounts, der jedes Konto mit seinen Nutzungsfenstern liefert. Der Projects-Bildschirm liest einen Agent beim Öffnen, wenn er für ihn noch nichts geladen hat, und liest danach beide Agents alle 60 Sekunden. Die Gruppe Agents in den Einstellungen liest einmal für einen Agent, den sie noch nie gesehen hat.

Ein Codex-Lesevorgang mit nur einer guten Antwort gilt trotzdem. Das Telefon behält den anderen Wert, den es schon hatte, und zeigt den Fehler des gescheiterten Aufrufs. Der Claude-Decoder ist streng: Er lehnt die ganze Antwort ab, wenn zwei Konten dieselbe ID haben, wenn nicht genau ein Konto aktiv ist oder wenn ein Nutzungsfenster eine andere Art als session oder weekly hat.

Eine Kachel oder Zeile zeigt einen von drei Zuständen: Nutzungsfenster, einen kurzen Hinweis wie „Not set up“ oder „Unavailable“ oder „Checking“. „Checking“ bedeutet, dass ein Lesevorgang läuft oder noch keine Antwort angekommen ist.

| Kontowechsel | Auf dem Telefon abgelehnt, wenn | Nach der Antwort |
| --- | --- | --- |
| Codex-Abonnement | Das Profil ist nicht angemeldet, ist schon aktiv, oder ein anderer Wechsel läuft. | Die Profile gelten, die Limits werden unbekannt, und ein neuer Lesevorgang für Limits und Profile startet. |
| Claude-Konto | Das Konto ist nicht angemeldet, ist schon aktiv, oder ein anderer Wechsel läuft. | Die Antwort enthält jedes Konto mit seiner Nutzung und gilt sofort. |

Ein Codex-Wechsel sendet system.activateCodexAuthProfile mit dem Schlüssel system.activateCodexAuthProfile:<profileId>:<uuid>. Ein Claude-Wechsel sendet system.activateClaudeAccount mit dem Schlüssel system.activateClaudeAccount:<accountId>:<uuid>. Die UUID ist bei jedem Tippen neu.

Jede Anfrage merkt sich den Desktop, der beim Absenden ausgewählt war. Eine Antwort, die ankommt, nachdem Sie einen anderen Desktop gewählt haben, wird verworfen. Die Wahl eines Desktops leert außerdem beide Kontozustände, sodass die Kacheln wieder mit „Checking“ beginnen.

Der Bildschirm eines Agents lädt dessen Nutzungsverlauf über system.agentUsageHistory mit dem Namen des Agents. Die Antwort enthält recordingSince und die wöchentlichen Messpunkte des aktiven Kontos. Für Codex kann sie außerdem Tokens pro Tag und eine Token-Übersicht mit der Gesamtzahl, dem stärksten Tag sowie der aktuellen und der längsten Serie enthalten. Ein Neuladen, das scheitert, behält den Verlauf, der schon auf dem Bildschirm steht.

- [Grenzen](https://www.hbcode.ai/de/docs/agents.md#limits): Woher der Desktop die Limits jedes Agents bekommt.
- [Agenten-Konten](https://www.hbcode.ai/de/docs/accounts-and-auth.md#agent-accounts): Codex-Profile und Claude-Konten auf dem Desktop.

## Ein Cache für Timeline-Projektionen

Ein TimelineTarget besteht aus einer App-Session, einem Projektordner, einem optionalen Thread für einen Subagent und einem Desktop. Projektionen für Eltern-Sessions und Subagent-Threads leben im Arbeitsspeicher des ViewModels, geordnet nach Ziel, und der Cache wird nach jeder Reducer-Aktion bereinigt. Die sichtbare Eltern-Session, der gewählte Thread und Ziele, die im Abgleich ausstehen oder laufen, sind geschützt. Vom Rest wird eine Projektion nach 12 Stunden Untätigkeit verdrängt, und jenseits von 64 Projektionen gehen die am längsten nicht genutzten zuerst, bei Gleichstand entschieden nach Session, Desktop, Projekt und Thread.

Ein Snapshot, der angefordert wurde, als sein Ziel sichtbar war, kann fertig werden, nachdem Sie weggewechselt haben. Er aktualisiert trotzdem die zwischengespeicherte Projektion dieses Ziels und kann weder die sichtbare Timeline berühren noch weitere Arbeit einplanen.

## Ein Tor für inhaltstragende Anfragen

WorkspaceTimelineSyncGate entscheidet, ob Timeline-Arbeit überhaupt laufen darf. Sein Bereich ist das Workspace-Ziel mit einer Session, einem Projektordner, einem Desktop und dem gewählten Thread, und er existiert nur, solange die Activity sichtbar ist. Jede Änderung dieses Bereichs, auch das Verbergen und Zeigen der Activity, erhöht eine Generation. Eine Anfrage erfasst die Generation und die Live-Event-Revision, und eine Anfrage nach dem neuesten Fenster zieht zusätzlich eine Nummer aus einer globalen Snapshot-Zulassungssequenz. Ein Ergebnis wird nur angewendet, solange seine Anfrage noch den sichtbaren Bereich besitzt, was auch eine Antwort abweist, die zurückkommt, nachdem Sie von Chat A zu Chat B und zurück zu A gewechselt sind.

## Ein oder zwei Ziele pro Anfrage, bei Fehlern geteilt

**Der Desktop lässt einen Batch als Ganzes scheitern, also teilt das Telefon den Batch, bis die fehlerhafte Session allein ist**

Ein Beispiel mit einer Eltern-Timeline von 2 MiB und einer Thread-Timeline von 9 MiB, maßstabsgetreu gezeichnet. Eine Abgleichsanfrage gelingt nur, wenn jede Session lädt und alle Ergebnisse in 8 MiB passen, also scheitert der erste Versuch. Das Telefon halbiert den Batch und sendet jede Hälfte erneut. Die Eltern-Timeline wird angewendet, und der Thread allein ist weiterhin zu groß. Nach etwa 0,75 s wird der Thread mit 12 statt 100 Zeilen angefragt, und er passt.

- **Batch** (run.chatReconcileSessions): Bis zu 16 Ziele aus einem Projektordner, ganz oder gar nicht beantwortet. Eine scheiternde Session oder Ergebnisse, die zusammen 8 MiB überschreiten, lassen die ganze Anfrage scheitern.
- **Halbierung** (isolateBatchFailure): Ein gescheiterter Batch wird halbiert, und jede Hälfte läuft erneut, rekursiv. Nur ein Ziel, das allein scheitert, wird veraltet, und seine Zeilen bleiben sichtbar. Antwortet der Desktop gar nicht, wird am Ende jedes Ziel veraltet.
- **Kleinere Seite** (TIMELINE_SNAPSHOT_TOO_LARGE): Ein einzelnes Ziel, das weiterhin zu groß ist, wiederholt mit 12 statt 100 Zeilen, bis zu 3 Mal, nach etwa 0,75 s, 1,5 s und 3 s (±20 %, nie über 3 s).
- **Tor** (WorkspaceTimelineSyncGate): Nur die sichtbare Eltern-Timeline und ihr ausgewählter Thread werden gesendet. Andere Ziele bleiben in der Warteschlange.

Veraltete Ziele reihen sich im Abgleich-Koordinator ein und laufen über run.chatReconcileSessions ab. Der Koordinator nimmt pro Anfrage bis zu 16 Ziele eines Projektverzeichnisses, aber erst, nachdem das Gate die Warteschlange gefiltert hat. Das Gate lässt die sichtbare Eltern-Timeline und ihren ausgewählten Thread zu, sodass ein Batch ein oder zwei Ziele hat. Eine Anfrage fragt 100 Zeilen pro Ziel an. Ziele, denen der sichtbare Bereich nicht mehr gehört, werden abgebrochen, bevor die Anfrage hinausgeht. Die Antwort muss für jedes angefragte Ziel genau eine Session enthalten, zugeordnet über die exakte Identität. Jede Session trägt außerdem outboxEntries, die Warteschlange des Desktops für diese Session, und das Telefon wendet sie zusammen mit der Seite an.

Eine gescheiterte Anfrage, ein Dekodierfehler oder eine Antwort, die nicht passt, teilt den Batch in zwei Hälften und führt jede Hälfte erneut aus, rekursiv, bis ein einzelnes Ziel allein scheitert. Dieses Ziel wird mit seinem Fehler als veraltet markiert, während seine Zeilen sichtbar bleiben. Ein Fehler TIMELINE_SNAPSHOT_TOO_LARGE teilt den Batch genauso, und ein einzelnes Ziel versucht es dann bis zu 3 Mal mit 12 Zeilen statt 100, nach 750 Millisekunden, verdoppelt bis höchstens 3 Sekunden, mit 20 Prozent Jitter.

## Live-Overlays und neueste Fenster

Ein blocks-Event wird nur angewendet, wenn seine Settlement-Epoche der aktuellen entspricht und seine Revision mindestens die aktuelle ist, und eine gleiche Revision muss identische Blöcke tragen. Eine neuere Epoche oder eine gleiche Revision mit anderen Blöcken markiert das Ziel als veraltet und reiht einen Abgleich ein, und alles Ältere wird verworfen. Das Overlay der Eltern-Session und das des gewählten Threads werden getrennt geführt.

Ein neues neuestes Fenster behält die älteren Zeilen, die das Telefon schon geladen hat, wenn ihre exakten Identitäten mit dem neuen Fenster übereinstimmen. Die erste Seite hält 12 Zeilen mit 115 Sekunden Zeitlimit und bis zu 3 Wiederholungen, ältere Seiten halten 100 Zeilen.

## LazyColumn-Schlüssel, Anker und die Live-Kante

Timeline-Zeilen verwenden den Schlüssel type::id und einen Inhaltstyp pro Zeilenart, neben festen Elementen für den Fehlerzustand, einen Unterhaltungsanker und die Zeile zum Laden älterer Einträge. Bevor ältere Zeilen eingefügt werden, merkt sich die Liste den ersten sichtbaren Index, seinen Offset und seinen Schlüssel. Danach stellt sie per Schlüssel wieder her oder verschiebt um die Zahl der eingefügten Zeilen, wenn der Schlüssel fehlt. Ältere Zeilen laden nur nach echtem Scrollen Richtung Anfang, beginnen innerhalb eines Bands von 8 Elementen und müssen nach jedem Laden neu scharf geschaltet werden.

Die Liste folgt der Live-Kante, solange Sie höchstens 72 Pixel vom Ende entfernt sind. Das Folgen bewegt sich mit der verbleibenden Strecke geteilt durch 1,5 Sekunden, begrenzt auf 80 bis 2.200 Pixel pro Sekunde, und rastet nach 2 stabilen Frames innerhalb von 1 Pixel ein.

MarkdownPreparationWorker parst Markdown auf Dispatchers.Default. Composables, die auf denselben Text warten, teilen sich einen Job, und der Job wird abgebrochen, wenn der letzte von ihnen geht. Ergebnisse werden zwischengespeichert, bis zu 256 Dokumente und 16 MiB, und ein Dokument ab 64 KiB wird in höchstens 24 Blöcke der obersten Ebene geteilt, die verzögert gerendert werden. Compose liest den Cache über volatile Snapshots, ohne auf die Sperre des Workers zu warten, und wenn eine streamende Zeile durch ihre abgeschlossene Zeile ersetzt wird, geht der aufbereitete Frame mit, sodass der Text nicht flackert.

Eine Zeile, die ein Link öffnet, kommt in einem Schritt ins Bild. WorkspaceTimelineRowPreparation lässt die Liste die Zielzeile und die Zeilen davor und danach zusammensetzen und messen, bis jede Seite eine Höhe der Timeline füllt. Die sichtbaren Zeilen ändern sich währenddessen nicht und nehmen Tipps an. Wenn das Markdown dieser Zeilen bereit ist, bewegt sich die Zeile einmal: Ihre Mitte geht zur Mitte der Liste, und die Liste hält an ihren Enden an. Nach 1 Sekunde bewegt sich die Zeile auch dann, wenn die Vorbereitung nicht fertig ist. Die Zeile eines Links pulsiert einmal.

Die schwebenden Bedienelemente stehen auf einer unteren Linie, in der Haupt-Timeline und im Thread-Viewer: das Menü, Scroll to latest und Stop. Jedes Bedienelement ist 52 dp groß, 16 dp vom Bildschirmrand und 10 dp von seinem Nachbarn entfernt. Seine Unterkante liegt 57 dp über der Navigationsleiste, und die letzte Zeile der Timeline liegt 125 dp über der Navigationsleiste. Modifier.floatingSurface gibt jeder schwebenden Fläche denselben Ton, surfaceBright. Der Menüknopf, der Scroll-Knopf und die Hinweiskarte sind Flächen der Art Face: der Ton mit 94 Prozent, ein erhabener Schatten und ein Schimmer im dunklen Modus. Das Verbindungsbanner und die Zeile eines wartenden Steers sind Karten: der volle Ton, ein flacher Schatten und kein Schimmer. Der Menüknopf zeigt ein einziges Badge an seiner oberen Ecke auf der Endseite: die Zahl der wartenden Nachrichten, solange die Warteschlange welche hat, sonst die Zahl der aktiven Sub-Agents. Seine Inhaltsbeschreibung nennt beide Zahlen.

## Verschlüsselte Verwahrung jeder Sendung

**Das Telefon entfernt eine Nachricht erst dann von seinem Speicher, wenn der Desktop bestätigt, dass er die Nachricht hat**

Die Absicht wird vor der Übermittlung in verschlüsselten Speicher geschrieben. In diesem Beispiel speichert der Desktop die Nachricht, aber seine Quittung geht verloren, und die App wird beendet. Die Absicht überlebt im Speicher. Sobald der Desktop nach dem Neustart auf system.ping antwortet, liest das Telefon die Desktop-Outbox, findet die Operations-ID und entfernt die Absicht, ohne sie erneut zu senden.

- **Verschlüsselte Absicht** (SharedPreferencesWorkspaceOutboxStore): Ein AES-GCM-Blob für alle wartenden Absichten, an den SHA-256 aus Region und Principal gebunden und vor dem RPC mit commit() geschrieben. Ein falscher Besitzer oder ein unlesbarer Blob lässt Schreibvorgänge scheitern, statt ihn zu ersetzen.
- **Quittung** (run.chatOutboxSubmit): Übermittlungen für eine Session laufen nacheinander. Eine Quittung, die Queue- und Operations-ID zurückgibt, entfernt die Absicht sofort. Ohne Quittung innerhalb von 60 s wird der Eintrag erneut eingereiht.
- **Abgleichen** (run.chatOutboxLoad): Läuft für jedes Ziel auf dem ausgewählten Desktop, sobald er auf system.ping antwortet. Absichten, die der Desktop aufführt oder die Timeline zeigt, werden entfernt, und eine Entfernung, die der Desktop noch aufführt, wird mit ihrem gespeicherten Schlüssel erneut gesendet.
- **Operations-ID** (android-<uuid>): Jedes erneute Senden verwendet sie wieder, und der Desktop gleicht sie ab, bevor er etwas speichert. Das Bearbeiten eines wartenden Eintrags erzeugt eine neue Operations-ID. Sofort senden behält sie und erneuert nur den Idempotenzschlüssel.

Jede Sendung, Bearbeitung und Entfernung wird in den Outbox-Speicher geschrieben, bevor ein RPC das Telefon verlässt. Der Speicher hält einen verschlüsselten Blob für alle wartenden Absichten, jede mit ihrem Desktop, Projekt und ihrer Session, den Queue- und Operations-IDs, dem Idempotenzschlüssel, dem Prompt, Anhängen mit ihrem aufbewahrten lokalen Pfad, dem Snapshot von Modell und Reasoning, dem Zustellmodus und einer Entfernungs-Mutations-ID, falls eine aussteht. Schreibvorgänge verwenden commit() unter einem Mutex und kehren erst zurück, wenn die Daten auf der Festplatte liegen.

Der Blob ist an seinen Besitzer gebunden, den SHA-256 aus Region und Principal, wobei der Principal das JWT-Subject ist oder sonst die kleingeschriebene E-Mail-Adresse. Passt der Besitzer nicht oder lässt sich der Blob nicht entschlüsseln, meldet der Speicher unlesbare Verwahrung, und Schreibvorgänge scheitern, statt sie zu ersetzen. Ein verspäteter Übermittlungs-Schreibvorgang kann auch keine bereits festgehaltene Entfernung ersetzen.

Eine Operations-ID sieht aus wie android-<uuid>, und die Queue-ID ist queue-<Operations-ID>. Sofort senden behält beide und erneuert nur den Idempotenzschlüssel für die Übermittlung, run.chatOutboxSubmit:<uuid>. Eine Bearbeitung behält die Queue-ID und erzeugt eine neue Operations-ID, eine einfache UUID, und einen neuen Schlüssel, weil der bearbeitete Text eine neue Nachricht ist. Übermittlungen einer Session laufen in einer Spur, eine nach der anderen. Jede wartet auf ausstehende Speichervorgänge der Chat-Einstellungen, bricht ab, wenn der Eintrag inzwischen entfernt wurde, und verlangt, dass der Beleg dieselbe Queue-ID und Operations-ID zurückgibt. Eine Übermittlung kann expectedChatSettings mit model, reasoningEffort und revision tragen, entnommen aus dem Auswahl-Snapshot des Eintrags oder sonst aus den aktuellen Einstellungen der Session.

Entfernen, Umsortieren, Run-Abbruch und Stop and send now verwenden jeweils einen Schlüssel der Form outbox-<kind>-<uuid>, neu für jedes Tippen. Der Desktop dedupliziert die Zustellung über die Queue- und Operations-IDs in der Anfrage. Umsortieren sendet run.chatOutboxReorder mit der vollständigen Liste orderedQueueIds.

Ist der Desktop wieder bereit, liest der Warte-Koordinator run.chatOutboxLoad für jedes Ziel. Absichten, die der Desktop schon hat, werden abgeschlossen. Absichten, die ihm fehlen und auch in der Timeline nicht sichtbar sind, werden mit derselben Operations-ID erneut übermittelt, und eine Entfernung, die der Desktop noch auflistet, wird mit ihrem gespeicherten Schlüssel erneut gesendet.

Anhänge kommen aus der Dokumentauswahl des Systems und werden in den App-Cache kopiert, bis zu 1 GiB. Ein PNG mit höchstens 16 Megapixeln wird als JPEG mit Qualität 88 neu kodiert. Der Upload läuft über begin, Blöcke in der Blockgröße des Desktops, begrenzt auf 1 MiB und Base64-kodiert, und dann finish, mit deterministischen Schlüsseln der Form files.chatAttachmentUpload:<session>:<name UUID>, gefolgt von :begin, :chunk:<n>, :finish oder :cancel. Cancel wird nur nach einem Fehler gesendet, der nicht wiederholt werden darf.

- [Nachrichtenzustellung](https://www.hbcode.ai/de/docs/message-delivery.md#uncertain-delivery): Was der Desktop mit einer Operation tut, deren Ergebnis unsicher ist.
- [Zwei Idempotenzschichten](https://www.hbcode.ai/de/docs/relay-protocol.md#idempotency): Wie der Desktop für einen wiederholten Schlüssel eine gespeicherte Antwort wiederholt.

## Zustellmodi, Steuern und Stop and send now

Jeder Eintrag hat einen von vier Zustellmodi. Der Modus bestimmt die beiden Wire-Felder intent und sendMode von run.chatOutboxSubmit.

| Zustellmodus, mit intent und sendMode | Gewählt, wenn |
| --- | --- |
| FrontQueue: automatic, queue | Sie senden, und die Session hat keinen Run und keinen wartenden oder sendenden Eintrag. Sofort senden bei einem wartenden Eintrag verwendet ihn ebenfalls. |
| Queue: enqueue, queue | Sie senden, kein Run ist aktiv, und die Session hat schon einen wartenden oder sendenden Eintrag. |
| Steer: automatic, steer-running | Sie senden, während ein Run aktiv ist, in einer Codex-Session und in einer Claude-Session. Die Anfrage nennt diesen Run in expectedActiveRunId. |
| Interrupt: replace-latest, interrupt-running | Sie ersetzen Ihre zuletzt gesendete Nachricht in einer Codex-Session. |

Der Anfrage-Builder erzwingt die Paarung: steer-running ohne expectedActiveRunId wirft eine Ausnahme, ebenso expectedActiveRunId mit jedem anderen sendMode. Der Outbox-Decoder wendet dieselbe Regel auf Einträge an, die der Desktop zurückgibt. Ein Steer, der angefordert wird, während kein Run aktiv ist, wird zu einem FrontQueue-Eintrag.

**Ein Steer wartet auf den aktuellen Schritt, und Stop and send now verschiebt ihn in einen eigenen Turn**

Drei Spuren, nicht maßstäblich. Ohne Run geht eine Sendung als automatic mit queue hinaus und startet Turn 1. Eine Sendung während des Runs geht als automatic mit steer-running hinaus und nennt den Run, den sie erwartet. Der Desktop antwortet mit einem Annahmebeleg, und der Eintrag zeigt „Sends after the current step“. Ein Tippen auf Stop and send now sendet den erwarteten Run, Thread und Turn. Der Desktop stoppt Turn 1 und gibt der Nachricht einen eigenen Turn. Die Spuren sind für eine Codex-Session und für eine Claude-Session gleich.

- **Vorn einreihen** (intent automatic · sendMode queue): Die normale Sendung, wenn die Session keinen Run und keine aktive Outbox-Zeile hat. Mit einer aktiven Zeile und ohne Run ist die Sendung ein einfacher Warteschlangen-Eintrag: enqueue mit queue.
- **Steuern** (steer-running · expectedActiveRunId): Die normale Sendung während eines Runs. Die Anfrage muss den aktiven Run nennen, und kein anderer Modus darf expectedActiveRunId tragen. Der Agent wendet einen angenommenen Steer nach seinem aktuellen Schritt an.
- **Annahmebeleg** (accepted { runId, threadId, turnId, acceptedAt }): Der Desktop fügt ihn dem Outbox-Eintrag hinzu. Alle vier Felder sind Pflicht. Stop and send now erscheint nur bei einem Eintrag im Zustand Sending mit einem Beleg für den aktiven Run und ohne Übergabe.
- **Stop and send now** (run.chatOutboxStopAndSendNow): Eine Anfrage pro Operation mit dem Schlüssel outbox-stop-and-send-now-<uuid>. Der Eintrag trägt danach handoff.phase: requested, replay-authorized, dedicated. Eine gescheiterte Anfrage zeigt „Not confirmed“.

Der Agent wendet einen angenommenen Steer erst nach seinem aktuellen Schritt an: Codex nach dem aktuellen Modellschritt und dessen Tool-Aufrufen, Claude zwischen zwei Tool-Runden. Bis die Nutzerzeile ankommt, zeigt die Timeline die Nachricht mit der Beschriftung „Sends after the current step“. Die Aktion Stop and send now erscheint daneben unter vier Bedingungen: Der Eintrag ist Sending, er trägt einen Annahmebeleg, der Beleg nennt den aktiven Run, und der Eintrag hat keine Übergabe. Wenn das Telefon den aktiven Turn aus einer laufenden Befehls- oder Tool-Zeile kennt, muss der Beleg auch diesen Turn nennen.

Die Aktion sendet run.chatOutboxStopAndSendNow mit der Session, der Queue-ID, der Operations-ID und dem erwarteten Run, Thread und Turn aus dem Beleg. Der Desktop prüft die drei erneut. Das Telefon sendet höchstens eine solche Anfrage pro Operation. Während sie läuft, ist die Aktion ausgeblendet. Wenn sie scheitert, wechselt die Beschriftung zu „Not confirmed“, und die Aktion kommt nicht zurück. Nach einem Erfolg trägt der Eintrag handoff.phase, einen der Werte requested, replay-authorized und dedicated, und ein Eintrag mit einer Übergabe bietet die Aktion nie wieder an.

- [Stoppen und unterbrechen](https://www.hbcode.ai/de/docs/codex-turns.md#stop): Was der Desktop mit dem laufenden Codex-Turn tut.
- [Claude-Live-Runs](https://www.hbcode.ai/de/docs/claude-agent-sdk.md#live-runs): Wann Claude eine Nachricht für den laufenden Turn liest und wie der Desktop sie zurückholt.

## Nachrichten zwischen Sessions

**Eine zurückgehaltene Nachricht aus einer anderen Session wartet in der Warteschlange, bis Sie sie senden. Wenn Sie sie bearbeiten, wird sie Ihre eigene Nachricht.**

Drei Spuren, nicht maßstäblich. Session A ruft send_session_message auf, und ihre Timeline zeigt eine Aktivitätszeile mit der Beschriftungszeile von dieser Session zu Session B. Der Desktop legt einen Eintrag in die Warteschlange von Session B und hält ihn zurück. Das Telefon zeigt den Eintrag mit der Beschriftungszeile des Desktops und dem Grund des Zurückhaltens. Ein Tippen auf Sofort senden reicht ihn ein, und Session B startet einen Turn, dessen Nutzerzeile die Beschriftungszeile von Session A zu dieser Session zeigt. Eine Bearbeitung macht den Eintrag stattdessen zu Ihrer eigenen Nachricht mit einer neuen Operations-ID.

- **Angabe message an einer Zeile** (message { direction, kind, from, to, peer, delivery, link }): Eine Nutzerzeile oder eine Aktivitätszeile kann sie tragen. Der Desktop erzeugt die Wörter der Beschriftungszeile. Eine Angabe, die eine Regel verletzt, weist die Zeile ab. Ein Tippen auf den Namen des anderen Endes öffnet dieses Ende an der Zeile der Nachricht.
- **Eintrag in der Warteschlange** (message · holdReason): Die Karte zeigt die Beschriftungszeile der Angabe message über dem Text und den Grund des Zurückhaltens darunter. Das Telefon zeigt und bearbeitet nur userText. promptText enthält den Umschlag des Desktops.
- **Sofort senden** (intent automatic · sendMode queue): Behält die Queue-ID und die Operations-ID und erneuert den Idempotenzschlüssel. Der Desktop wertet eine Sendung mit jedem anderen intent als enqueue als Ihre Freigabe.
- **Bearbeiten in der Warteschlange** (neue Operations-ID): Der bearbeitete Text ist eine neue Nachricht von Ihnen. Der Eintrag behält seine Queue-ID, bekommt eine neue Operations-ID und einen neuen Schlüssel und verliert die Angabe message und den Grund des Zurückhaltens.

Eine Nutzerzeile oder eine Aktivitätszeile kann message tragen, die typisierte Angabe einer Nachricht zwischen zwei Sessions oder zwischen zwei Agenten. Das Band zeigt ihre Beschriftungszeile in den Wörtern des Desktops, zum Beispiel „This session → Session B“ oder „Claude → This agent“, das Signal Sent, Needs approval oder Not sent und den Text als Markdown. Der Decoder ist streng: Eine Angabe mit einem Wert, der kein Leitungswert ist, oder mit einem leeren Wort weist die Zeile ab. Das Telefon erzeugt keine eigenen Wörter.

Wenn die Angabe einen Link hat, ist der Name des anderen Endes in der Beschriftungszeile der Link: kräftiger türkiser Text mit der weichen Unterstreichung eines Markdown-Links und dem Agentenzeichen innerhalb der Tippfläche. Der Link ist für einen Screenreader ein eigener Knoten mit der Aktion „Go to the message in <peer>“, und die Plattform gibt ihm die minimale Tippfläche. Ein Tippen auf einen anderen Teil des Bands öffnet das Nachrichtendetail: den Titel, die Beschriftungszeile mit demselben Link und dem Signal, die Zusammenfassung, den Grund des Fehlers und den Text als Markdown. Das Detail kommt nur aus dem Band, weil eine Nachrichtenzeile auf der Leitung kein detail hat.

Ein Tippen auf den Link startet ein Laden mit Anker, MobileScreenState.anchoredLoad. Das Telefon öffnet die Session von link.appSessionId, sonst diese Session, und den Thread von link.threadId, sonst die Haupt-Timeline. Dann lädt es diese Timeline mit einem Link-Anker. Wenn die Antwort ankommt, bewegt sich die Zeile von anchorRow in die Mitte und pulsiert einmal. anchorNotice erscheint als kleine Karte oben in der Timeline, und ein Tippen schließt sie. Es gibt kein Zurück-Bedienelement: Der Weg zurück ist der Link auf der Zeile am anderen Ende. Das Laden endet, wenn Sie an eine andere Stelle gehen oder eine Nachricht senden.

Wenn eine Lückenwiederherstellung läuft, während die Timeline eine Seite in der Vergangenheit zeigt, bleibt die Seite. Die Timeline nennt die erste gespeicherte Zeile im Bild und ihren Versatz. Das Telefon lädt die Seite erneut mit einem Zeilenanker an dieser Zeile. Die Zeile kehrt ohne Pulsieren an denselben Versatz zurück, schon im ersten Frame, den die Timeline zeichnet.

Eine Nachricht, die noch wartet, ist ein Outbox-Eintrag der Empfänger-Session. Der Desktop sendet ihn mit message, der Angabe der Nutzerzeile, zu der er wird, und, wenn er die Zustellung zurückhält, mit holdReason. Die Warteschlangenkarte zeigt die Beschriftungszeile über dem Text und den Grund des Zurückhaltens darunter. Die Karte zeigt und bearbeitet nur userText, weil promptText den Umschlag des Desktops um die Nachricht enthält.

Sofort senden reicht den Eintrag mit unveränderter Queue-ID und Operations-ID als FrontQueue ein. Eine Bearbeitung ersetzt die Operations-ID und entfernt die Angabe message und den Grund des Zurückhaltens, sodass der Eintrag zu Ihrer eigenen Nachricht wird.

- [Nachrichten zwischen Sessions](https://www.hbcode.ai/de/docs/message-delivery.md#session-messages): Der Umschlag, die Grenzen und die Regeln zum Zurückhalten auf dem Desktop.
- [Das Tool für Session-Nachrichten](https://www.hbcode.ai/de/docs/tools/agent-tools.md#session-messages): Der Tool-Vertrag, den der Agent sieht.
- [Nachrichtenzeilen und das Laden mit Anker](https://www.hbcode.ai/de/docs/timeline.md#anchored-load): Die Angabe message, der typisierte Anker und das Fenster mit 40 Zeilen.

## Tests, die dieses Verhalten festhalten

- WorkspaceChatReconciliationRetentionTest: genau 64 behaltene Ziele, Verdrängung bei Untätigkeit vor der Obergrenze und sichtbare, ausstehende und laufende Ziele auch jenseits davon.
- WorkspaceChatReconciliationAdmissionTest und WorkspaceChatReconciliationProjectionTest: Ein scheiterndes Ziel wird veraltet, ohne seine Zeilen zu verlieren, und wiederholte Prompts mit gleichem Text werden nie über Text oder Zeitstempel zusammengeführt.
- HBCodeAndroidWorkspaceReconciliationCoordinatorTest: Eltern-Session und Thread in einer Anfrage, die Halbierung eines gescheiterten Batches und die begrenzten Wiederholungen einer übergroßen Timeline.
- TimelineLiveGrowthTest, ein instrumentierter Paritätstest: Eine gesendete Nachricht und später wachsende Zeilen bewegen die Liste in begrenzten Schritten vorwärts.
- WorkspaceAnchoredLoadReducerTest: Mit den gemeinsamen Antworten für Seiten in der Vergangenheit landet ein Link genau auf der Zeile, zwischen zwei Sessions und zwischen der Haupt-Timeline und einem Agent-Thread.
- WorkspaceMessageLinkRenderingTest, ein instrumentierter Test: Eine gelandete Zeile hat ihre Mitte in der Mitte der Liste, und nach einer Lückenwiederherstellung steht die Zeile des Lesers vom ersten Frame an an ihrer Stelle.

Aus mobile/android:

```
./gradlew :app:testDebugUnitTest
./gradlew :app:pixel2api35DebugAndroidTest
```
