Zum Artikel springen
HB CodeDocsApp herunterladen

HandbuchiOS-App

iOS: Chat-Hosts, die Collection-View und die Outbox

Wie die iPhone-App besuchte Chats behält, Snapshots gegen neueren Live-Zustand anwendet, die Timeline in einer UIKit-Collection-View darstellt, das Scrollen verankert, eine Sendung prüft, deren Bestätigung verloren ging, und die Warteschlange, eine angenommene Steuerung und Nachrichten aus anderen Sessions zeigt.

Gegen den Quellcode geprüft am 5. Oktober 2026

Auf dieser Seite

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

Das Formular für eine neue Session hat eine Agent-Auswahl. SessionAgent hat genau zwei Werte, codex und claude. Der Agent bleibt für die ganze Lebensdauer der Session, deshalb fragt das Formular vor session.create. Das Formular speichert die Wahl in UserDefaults unter session.newSessionAgent, und vor einer ersten Wahl ist Codex vorausgewählt. Die Auswahl ist deaktiviert, solange eine Anfrage zum Erstellen läuft.

Das Formular listet auch die gespeicherten Unterhaltungen des gewählten Agents für den Projektordner, über run.chatExistingSessions, und zeigt eine davon mit run.chatPreviewExistingSession. Ein Wechsel des Agents lädt die Liste neu. Eine Anfrage session.create mit Schlüssel trägt den Agent und, wenn Sie eine Unterhaltung gewählt haben, deren conversationId, und die neue Session setzt diese Unterhaltung fort. Eine Unterhaltungszeile zeigt ein Modell und einen Client-Namen nur für Codex, weil nur Codex-Threads sie melden.

Danach nutzt eine Claude-Session denselben Chat-Host, dieselben Zeilen und dieselben run.chat*-Methoden wie eine Codex-Session. Der Code des Telefons verzweigt nur an wenigen Stellen nach dem Agent.

TeilCodex-SessionClaude-Session
Familien in der Modellauswahlastra, sol, terra, luna. Die Matrix braucht sol, terra und luna.fable, opus, sonnet, haiku. Die Matrix zeigt die Familien, die der Desktop meldet.
GeschwindigkeitsmodusDer Eintrag trägt den Modus aus dem Composer.Der Eintrag trägt immer standard.
Namen der Zugriffsstufen, von read-only bis unrestrictedRead only, Full auto, Full access und Never ask.Plan mode, Accept edits, Auto mode und Bypass permissions.
storageFormat im Verlaufsumschlagindexednative

Die App hält bis zu sechs Chat-Hosts im Cache und zeigt einen

Ein geparkter Chat behält seine Zeilen. Live-Ausgabe kommt erst bei der Rückkehr.
Ein geparkter Chat behält seine Zeilen. Live-Ausgabe kommt erst bei der Rückkehr.Zwei Chats auf demselben Desktop im Zeitverlauf. Solange Chat A auf dem Bildschirm ist, hält er die Timeline-Lease, und seine Live-Blöcke kommen an. Wenn Sie Chat B öffnen, wird Chat A geparkt: Er behält seine Zeilen, aber seine neuen Blöcke werden nicht zugestellt, weil jetzt B die Lease hält und nur die Live-Blöcke von B ankommen. Chat B beginnt mit einem ersten Laden. Wenn Sie zu Chat A zurückkehren, führt ein einziges Laden die neueren Zeilen mit den behaltenen zusammen, der Chat erscheint wieder am Ende hinter einer kurzen Abdeckung, und seine Live-Blöcke kommen wieder an. Darunter hält der Host-Cache sechs Chats, den zuletzt gewählten zuerst. Ein siebter Chat gibt den am längsten nicht gewählten frei, der danach wieder mit einem ersten Laden beginnt.
Chat A lesen
Chat B öffnen
Zurück zu Chat A
Chat A
Desktop
Chat B
auf dem Bildschirm · Lease alle 20 s erneuert
geparkt: Zeilen bleiben, nichts live
wieder am Ende, hinter kurzer Abdeckung
auf dem Bildschirm · hält die Lease
Live-Blöcke von A
neue Blöcke von A: nicht zugestellt, keine Lease
Erstladen
B live
neuere Zeilen ergänzen den Verlauf
Host-Cachezuletzt gewählter zuerst
G
A
B
C
D
E
F
Chat 7 verdrängt F. F lädt danach neu.
  • Host-CacheWorkspaceChatHostCache

    Hält bis zu sechs Chat-View-Models, das zuletzt gewählte zuerst, und hängt nur das gewählte ein. Ein siebter Chat gibt das am längsten nicht genutzte frei.

  • Timeline-Leasechat:timeline-interest

    Nur der Chat auf dem Bildschirm hält sie. Das Telefon erneuert sie alle 20 Sekunden, und das Relay bewahrt sie 60 Sekunden auf. Ohne sie erreichen die Live-Blöcke dieses Chats das Telefon nie.

  • Nachladenrun.chatLoad

    Die Rückkehr startet ein Laden des neuesten Fensters, das in die behaltenen Zeilen einfließt. Ein Chat auf dem Bildschirm prüft außerdem etwa alle 60 Sekunden, oder alle 10, solange ein Run, eine Nachricht oder ein Laden aussteht.

WorkspaceChatHostCache behält die View-Models kürzlich besuchter Chats, geordnet nach Desktop, Projektordner und Session, und hält höchstens sechs. Die Auswahl eines Chats während eines SwiftUI-Renderings merkt seinen Eintrag nur vor, und die Aufgabe der Auswahl übernimmt ihn danach, sodass der Cache nie mitten in einer View-Aktualisierung eine Änderung veröffentlicht. Nur der ausgewählte Host ist eingehängt.

Jeder Host besitzt einen Scheduler für das neueste Fenster mit einer aktiven Operation und einer ausstehenden Anfrage für sein aktuelles Ziel. Anfragen werden zusammengefasst, und ein Snapshot wird nur angewendet, wenn sein Ziel und die erfassten Zäune noch passen. Ein geparkter Host behält seine Zeilen und sein Live-Overlay, und sein Scheduler startet nichts, bis der Host wieder ausgewählt und aktiv ist. Scroll-Position und Aufdeckungszustand gehören zur nicht eingehängten View, daher kehrt er am Ende hinter einer kurzen Abdeckung zurück. Die iPhone-App hat keinen Batch-Abgleich und lädt keinen Chat, den Sie gerade nicht ansehen.

Ein beendeter, gescheiterter, abgebrochener oder gestoppter Run, eine Desktop-Wiederherstellung, die Rückkehr in den Vordergrund, ein anderer gewählter Desktop, eine ausdrückliche Aktualisierung, die erneute Auswahl des Chats und ein neueres updatedAt der Session markieren das gewählte Ziel als veraltet und können sein Laden einreihen. Solange der Chat auf dem Bildschirm bleibt, lädt er außerdem etwa alle 60 Sekunden das neueste Fenster, oder alle 10 Sekunden, solange ein Run, eine Nachricht oder ein Laden aussteht, und auf jede Wartezeit kommen bis zu 30 Prozent hinzu. Dieses regelmäßige Laden läuft nur, wenn die App aktiv ist, der Desktop eine Anfrage annehmen kann, das Relay sich nicht neu verbindet, kein Laden aktiv ist oder aussteht, keine ältere Seite lädt und die Timeline keine leere ist, die der Desktop noch nicht bestätigt hat. Ein gescheitertes Laden lässt die sichtbaren Zeilen, wie sie sind, und markiert den Host für ein frisches Laden bei seiner nächsten Aktivierung. Subagent-Threads öffnen sich in einem eigenen Viewer mit einem separaten View-Model, das über run.chatLoadThread und run.chatLoadThreadPageBefore lädt.

Die erste Seite hat 12 Zeilen, eine ältere Seite 100. Ein gescheitertes erstes Laden wird bis zu 3 Mal wiederholt. Die Wartezeit beginnt bei 0,75 Sekunden, verdoppelt sich mit jedem Versuch, schwankt um 20 Prozent in beide Richtungen und überschreitet nie 3 Sekunden. Zwei Neuladevorgänge desselben Chats starten mindestens 2 Sekunden auseinander.

Der Verlaufsumschlag wird exakt dekodiert

WorkspaceChatLoadEnvelope verlangt genau die kanonische Menge an Schlüsseln auf oberster Ebene. Mehrere Felder müssen vorhanden sein, auch wenn ihr Wert null ist. Der Decoder prüft ein nicht leeres boundaryToken und dekodiert Timeline-Blöcke über den gemeinsamen Seitendecoder. Jede Zeile braucht einen type aus user, system, activity, agent-reply und thinking, eine nicht leere id, einen origin von canonical oder live, ein nicht leeres sourceOrder aus vorzeichenlosen Ganzzahlen und einen ganzzahligen timestamp. Live-Zeilen brauchen eine runId, und Live-Nutzerzeilen brauchen eine operationId. Ein doppelter Splice-Schlüssel lässt die ganze Seite scheitern.

Felder der initialen Verlaufsantwort — Schema-Übersicht
boundaryToken: nonblank string
nativeSessionId: string | null
loadState: typed load state
storageFormat: "indexed" | "native"
loadedEntries: integer
hasOlderEntries: boolean
oldestCursor: string | null
timeline: typed blocks[]
liveOverlay: { settlementEpoch, revision, blocks }
runtime: object | null
activeRun: object | null

Der Live-Overlay-Decoder erfordert exakte ganzzahlige Epochen- und Revisionswerte innerhalb des sicheren JavaScript-Bereichs. Eine Revision von null kann keine Blöcke enthalten. Jeder Overlay-Block muss den Ursprung live haben, und typisierte Blockidentitäten müssen eindeutig sein. Das Zurückweisen eines ungültigen Envelopes verhindert, dass fehlerhafte Daten zu einem scheinbar gültigen leeren Verlauf werden.

Ein Snapshot wird auf einen neueren Live-Zustand angewendet

Ein verspäteter Snapshot fügt seine Historie hinzu, setzt die Live-Zeilen aber nie zurück
Ein verspäteter Snapshot fügt seine Historie hinzu, setzt die Live-Zeilen aber nie zurückDas Chat-View-Model fordert beim Desktop das neueste Fenster an und merkt sich seinen Live-Event-Zähler, in diesem Beispiel 7. Der Desktop liest das Fenster bei Overlay-Revision 41, doch die Antwort braucht länger als ein Live-Event mit Revision 42, das das View-Model sofort zeigt und das den Zähler auf 8 erhöht. Wenn der Snapshot ankommt, prüft das View-Model zuerst sein Overlay, das die ganze Antwort bei einer älteren Epoche oder bei gleicher Revision mit anderen Blöcken verwerfen würde. Weil sich der Zähler bewegt hat, werden die Live-Zeilen und der aktive Run der Antwort verworfen. Ihre kanonischen Zeilen und die Paginierung werden übernommen, und ihr Overlay mit Revision 41 wird ignoriert, also bleibt Revision 42 auf dem Bildschirm.
Desktop
Chat-View-Model
neuestes Fenster laden
Zähler 7 gemerkt
liest das Fenster bei Revision 41
Snapshot · Revision 41
Live-Blöcke · Revision 42
Revision 42 angezeigt · Zähler 8
Snapshot kommt an
In diesem Beispiel würde der Snapshot, so angewendet, wie er ankam, Revision 41 zurück auf den Bildschirm bringen.
ältere Epoche, oder gleiche Revision mit anderen Blöcken?
ja: verwerfen und neu laden
Zähler von 7 auf 8 gestiegen
seine Live-Zeilen und der aktive Run fallen weg
Verlaufsfenster
Verlauf und Seiten übernehmen
Overlay-Revision 41 gegen 42
ignoriert: Revision 42 bleibt sichtbar
  • Live-Event-ZählertimelineLiveEventRevision

    Steigt, wenn ein Live-Event die Overlay-Revision erhöht oder ein Run beginnt oder endet. Jedes Laden des neuesten Fensters merkt sich den Wert, mit dem es begann.

  • Overlay-VersionsettlementEpoch · revision

    Innerhalb einer Epoche gewinnt die höhere Revision, und eine niedrigere wird ignoriert. Ein Live-Event mit neuerer Epoche wird abgelehnt und löst ein erneutes Laden des neuesten Fensters aus.

  • Veraltete TeileremovingStaleLiveRows

    Hat sich der Zähler während des Ladens bewegt, werden die Live-Zeilen und der aktive Run der Antwort verworfen. Ihre kanonischen Zeilen gelten weiterhin.

Ist die Historie nicht verfügbar, bleiben das sichtbare Fenster und die Paginierung erhalten, und nur ein Overlay aus derselben Epoche wird angewendet. Danach werden Outbox-Einträge und Echos mit der bestätigten Historie abgeglichen, die Timeline-Revision steigt, wenn sich etwas geändert hat, und ein Aktualisieren, das ein nicht leeres Fenster durch Zeilen ohne Überschneidung ersetzt, rückt die Darstellungsepoche vor.

Eine UIKit-Collection-View in SwiftUI

Streaming zeichnet geänderte sichtbare Zellen höchstens sechsmal pro Sekunde neu und wartet beim Scrollen.
Streaming zeichnet geänderte sichtbare Zellen höchstens sechsmal pro Sekunde neu und wartet beim Scrollen.Zwei Sekunden eines streamenden Chats, maßstabsgetreu gezeichnet. Änderungen am Live-Overlay erreichen das View-Model höchstens alle 100 Millisekunden. SwiftUI rendert sie höchstens sechsmal pro Sekunde, und jedes Rendering konfiguriert nur die sichtbaren Zellen neu, deren Inhalt sich geändert hat. Kommt eine Zeile hinzu, wendet der Koordinator einen Diffable-Snapshot ohne Animation an. Während Sie die Liste ziehen oder sie ausläuft, warten Änderungen an sichtbaren Zeilen oder an der Zeilenliste, und das Rendern läuft weiter. Alle 250 Millisekunden läuft eine Prüfung, und sobald die Liste ruht, gilt nur die neueste zurückgehaltene Aktualisierung.
Text strömt in eine Zeile
Eine Zeile kommt hinzu
Liste ziehen
Overlay-Änderungen
Renderings
Collection-View
SwiftUI zu UIKit
höchstens einmal pro 100 ms
Rendern: max. alle 1/6 s
gleiche Item-IDs: nur geänderte sichtbare Zellen werden neu konfiguriert
neue Item-ID: ein Snapshot, keine Animation
gehalten beim Ziehen
alle 250 ms geprüft
in Ruhe: die neueste gehaltene Aktualisierung gilt einmal
0 s
0,5 s
1 s
1,5 s
2 s
  • Render-TaktliveStreamingRenderMinInterval

    Änderungen am Live-Overlay erreichen SwiftUI höchstens alle 1/6 Sekunde, oder alle 0,5 Sekunden, solange der Composer oder eine Vorschau offen ist.

  • KoordinatorWorkspaceChatTimelineCollectionView.Coordinator

    Vergleicht Item-IDs mit dem angewendeten Snapshot. Bei gleichen IDs konfiguriert er nur die geänderten sichtbaren Zellen neu, und Zeilen außerhalb des Bildschirms übernehmen neue Inhalte, wenn sie hereinscrollen.

  • AnzeigezeilenWorkspaceChatTimelineLayerProjectionCache

    Vom Chat-Tab aus den Zeilen des View-Models gebaut. Thinking-Zeilen verlassen die Liste, das Live-Thinking des aktiven Runs erscheint als angehefteter Status, und Subagent-Panel-Zeilen werden ausgeblendet.

  • Zurückgehaltene AktualisierungdeferredUpdateRecoveryIntervalNanoseconds

    Während Sie ziehen oder die Liste ausläuft, warten Änderungen an sichtbaren Zeilen oder an der Zeilenliste. Nur die neueste wird angewendet, sobald die Liste stoppt oder eine Prüfung alle 250 ms sie in Ruhe findet.

Die Timeline ist eine UICollectionView in einem UIViewRepresentable. Sie nutzt ein Compositional Layout mit einer vertikalen Section und geschätzten Höhen von 80 Punkten, eine Diffable Data Source, deren Elemente Zeilen-ID-Strings sind, und Zellen, deren Inhalt eine UIHostingConfiguration um die SwiftUI-Zeile ist. Die Invalidierung der Selbstgröße schließt Constraints ein, sodass eine wachsende Zeile neu gemessen wird. Der Chat-Tab baut die Anzeigezeilen aus den Zeilen des View Models: Thinking-Zeilen verlassen die Liste, und das Live-Thinking des aktiven Runs wird zum angehefteten Status. Der Desktop sendet keine Zeile für einen Subagent oder einen Workflow. Die Collection selbst fügt nie eine Nachricht hinzu und lässt nie eine weg.

Änderungen am Live-Overlay erreichen SwiftUI höchstens sechsmal pro Sekunde, oder zweimal pro Sekunde, solange der Composer oder eine Vorschau offen ist, und während eines Ziehens bleibt nur die neueste zurückgehaltene Aktualisierung erhalten.

Erste Anzeige, ältere Seiten und das Folgen am unteren Ende

Ein neu gezeigter Chat bleibt abgedeckt, bis sein Layout drei Durchläufe lang gleich bleibt, und nie länger als etwa zwei Sekunden
Ein neu gezeigter Chat bleibt abgedeckt, bis sein Layout drei Durchläufe lang gleich bleibt, und nie länger als etwa zwei SekundenDrei erste Aufdeckungen, maßstabsgetreu über zwei Sekunden gezeichnet, mit einer Markierung pro Settle-Durchlauf von 32 Millisekunden. Im ersten Fall warten drei Durchläufe darauf, dass eine Markdown-Zeile fertig aufbereitet ist, ein Durchlauf findet das Layout verschoben, und drei stille Durchläufe folgen, also fällt die Abdeckung nach etwa 0,22 Sekunden. Im zweiten Fall ist der Verlauf kurz: Das Layout kommt zur Ruhe, eine ältere Seite wird angefordert, Durchläufe warten darauf, und die Abdeckung fällt, sobald das Layout wieder ruht. Im dritten Fall ändert das Layout ständig seine Größe, die Zahl stiller Durchläufe erreicht nie drei, und die Abdeckung fällt trotzdem bei Durchlauf 63, nach etwa zwei Sekunden.
Zeilen kommen zur Ruhe
Kurzer Verlauf
Ändert ständig die Größe
Durchlauf 63: die Abdeckung fällt trotzdem
Markdown abwarten, 1 bewegt, 3 still
aufgedeckt nach etwa 0,22 s
stabil, ältere Seite anfordern
wartet auf die Seite
wieder zur Ruhe: aufgedeckt
ändert ständig die Größe: die Zählung erreicht nie drei
0 s
0,5 s
1 s
1,5 s
2 s
Solange die Abdeckung liegt, ist die Liste verborgen und ignoriert Berührungen. Ein Settle, der läuft, nachdem der Chat schon sichtbar war, endet, sobald Sie ziehen.
  • Settle-DurchlaufpassDelayNanoseconds

    Alle 32 ms vergleicht die View Grenzen, Inhaltshöhe, Offset, Insets sowie Oberkante und Höhe jeder sichtbaren Zeile mit dem Durchlauf davor. Still heißt innerhalb von 0,5 pt, daher zählt der erste Durchlauf nach einem Reset nie.

  • Aufbereitete ZeilenvisiblePreparedMarkdownRowsAreReady

    Durchläufe zählen nicht, solange eine sichtbare Markdown-Zeile aufbereitet oder ein Snapshot angewendet wird.

  • Kurzer VerlaufolderTimelinePreloadThreshold

    Liegen höchstens 1.800 pt über dem Bildschirm, fordert die ruhende View eine ältere Seite an und kommt erneut zur Ruhe, bis zu zwei Seiten, bevor die Abdeckung fällt.

  • FristmaximumSettlementPassCount

    Nach 63 Durchläufen, etwa zwei Sekunden, fällt die Abdeckung, auch wenn das Layout nie stillhielt.

Die Abdeckung fällt nach etwa zwei Sekunden, auch wenn das Layout nie stillhält, und ein kurzer Verlauf lädt darunter zuerst bis zu zwei ältere Seiten. Die Darstellungsepoche verbindet die Session mit einer Timeline-Identität pro Viewport, parent-chat oder subagent-<thread ID>. Sie rückt vor, wenn ein Aktualisieren das Fenster durch Zeilen ohne Überschneidung ersetzt, wenn ein leerer, unbestätigter Chat ein frisches Laden startet oder wenn der Thread wechselt, sodass Scroll-Annahmen eines Fensters nie in ein anderes übergehen.

Ältere Seiten laden, wenn Sie bis auf 1.800 Punkte an den Anfang scrollen, höchstens alle 0,25 Sekunden, und ein Ladevorgang gilt nach 8 Sekunden als veraltet. Bevor Zeilen vorangestellt werden, merkt sich die Ansicht die erste sichtbare Zeile und ihren Offset, und nach der Aktualisierung scrollt sie diese Zeile zurück an ihren Platz und stellt den Offset wieder her. Live-Zeilen werden bis zu 1,5 Sekunden zurückgehalten, solange die Wiederherstellung läuft, damit eine streamende Antwort die Zeilen, die Sie lesen, nicht verschiebt.

Das Folgen am unteren Ende läuft auf einem CADisplayLink. Die Geschwindigkeit ist die verbleibende Strecke geteilt durch 1,5 Sekunden, begrenzt auf 80 bis 2.200 Punkte pro Sekunde, und mit aktivierter Option „Bewegung reduzieren“ springt die Ansicht stattdessen. Einrastdurchläufe laufen alle 32 Millisekunden, oder 350 Millisekunden nach einem animierten Scrollen, und enden nach zwei stabilen Durchläufen innerhalb von 1 Punkt und einem halben Punkt Höhenänderung oder nach acht Durchläufen.

Die schwebenden Bedienelemente stehen auf einer unteren Linie, in der Haupt-Timeline und im Thread-Viewer: das Menü, Scroll to latest, Stop und Switch desktop. WorkspaceChatBottomChrome besitzt diese Linie. Jedes Bedienelement ist 52 Punkte breit, 16 Punkte vom Bildschirmrand und 10 Punkte von seinem Nachbarn entfernt. Die Unterkante jedes Bedienelements liegt 57 Punkte über dem unteren sicheren Bereich. Die Run-Uhr und der angeheftete Status liegen unter der Linie und nehmen ihr keinen Platz, und die letzte Zeile der Timeline liegt 125 Punkte über dem sicheren Bereich. Das Menü, Scroll to latest und Switch desktop haben eine gemeinsame Fläche, WorkspaceFloatingControlFace: die schwebende Farbe mit 94 Prozent, einen Schimmer im dunklen Modus und einen Schatten. Der geschlossene Menüknopf zeigt ein einziges Badge: die Zahl der wartenden Nachrichten, solange die Warteschlange welche hat, sonst die Zahl der aktiven Sub-Agents. Sein VoiceOver-Label nennt beide Zahlen.

Die Kopie der Outbox auf dem Telefon

Die Zustellung gehört dem Desktop. Das Telefon hält eine Wiederholungsmenge in UserDefaults unter workspace-chat-local-outbox-v2, wobei jeder Session-Bereich seine Operationsreihenfolge speichert und jede Operation entweder als ausstehend, mit dem vollständigen Eintrag, oder als abgeschlossen. Eine abgeschlossene Operation sperrt veraltete Schreiber für den Rest des Prozesses aus und wird beim nächsten Start, der eine neue Generation beginnt, entfernt.

Feld des EintragsWer es setzt und was es bedeutet
queueId, operationIdVom Telefon gesetzt. Der Platz in der Warteschlange und die unveränderliche Nachricht. Beide dürfen nicht leer sein.
userText, promptTextVom Telefon gesetzt, bei einer Session-Nachricht vom Desktop. Der Text, wie ihn der Autor geschrieben hat, und der Text, den der Agent erhält.
statusVon beiden gesetzt. queued oder sending. Jeder andere Wert lässt das Dekodieren scheitern.
sendMode, expectedActiveRunIdVom Telefon gesetzt. queue, steer-running oder interrupt-running. Eine Steuerung muss eine nicht leere Run-ID tragen, und die anderen Modi dürfen keine tragen. Der Initializer, der Decoder und der Encoder prüfen das alle.
pendingMutationVom Telefon gesetzt. Die Art, submit oder remove, mit eigener Mutations-ID. Ein submit hält auch seine Absicht fest. Eine Wiederholung sendet dieselbe Anfrage noch einmal.
accepted-ObjektVom Desktop gesetzt. runId, threadId, turnId und acceptedAt des laufenden Turns, der eine Steuerung angenommen hat.
handoff-ObjektVom Desktop gesetzt. Ein laufendes Stop-and-send-now, mit der Phase requested, replay-authorized oder dedicated.
holdReasonVom Desktop gesetzt. Warum die Zustellung wartet. Das ist kein Fehler. Dafür gibt es das Feld error.
messageVom Desktop gesetzt bei einer Nachricht aus einer anderen Session: die Angabe message der User-Zeile, zu der der Eintrag wird, mit dem Titel des Absenders und „This session“. Sie hat keinen Link und keine delivery, und jede andere Angabe message lässt das Dekodieren scheitern. Sie fehlt, wenn Sie die Nachricht geschrieben haben.

Outbox-Änderungen einer Session laufen als serielle Kette. Eine Übermittlung wartet 20 Sekunden auf die Quittung des Desktops. Eine endgültige Ablehnung legt den Text zurück in den Composer, wenn sich der Entwurf wiederherstellen lässt.

Das Telefon kann eine verlorene Übermittlung nicht von einer verlorenen Quittung unterscheiden und prüft deshalb zuerst
Das Telefon kann eine verlorene Übermittlung nicht von einer verlorenen Quittung unterscheiden und prüft deshalb zuerstZwei Fälle mit demselben Zeitablauf, oben das Telefon, unten der Desktop. Im ersten geht die Übermittlung verloren, bevor sie den Desktop erreicht. Im zweiten speichert der Desktop die Nachricht und legt ihre Quittung unter dem Idempotenzschlüssel ab, aber die Quittung geht verloren. Das Telefon sieht in beiden Fällen dasselbe: Es wartet 20 Sekunden, lässt den Eintrag in der Warteschlange und lädt die Outbox des Desktops, sobald die Verbindung wieder nutzbar ist oder nach 10 bis 13 Sekunden. Im ersten Fall fehlt der Eintrag überall, also sendet das Telefon ihn mit demselben Schlüssel erneut, und der Desktop speichert ihn einmal. Im zweiten Fall ist der Eintrag aufgelistet, also wird nichts erneut gesendet. Hätte er die Outbox schon verlassen, ohne in der Timeline zu stehen, bekäme das erneute Senden mit demselben Schlüssel die gespeicherte Quittung zurück.
Wenn die Übermittlung den Desktop nie erreichte
Telefon
Desktop
Relay
wartet 20 s
Timeout: bleibt eingereiht
bis die Verbindung steht, oder 10–13 s
Outbox-Prüfung
fehlt
fehlt überall
senden
erneut, gleicher Schlüssel
einmal gespeichert
Wenn der Desktop sie speicherte und nur die Quittung verloren ging
Telefon
Desktop
Relay
wartet 20 s
aufgelistet
vorhanden: nicht erneut senden
gespeichert, Quittung mit Schlüssel
Quittung
Wenn der Eintrag schon aus der Outbox entfernt ist, liefert erneutes Senden die gespeicherte Quittung.
  • Unbekannter AusgangServerRelayError.timeout

    Ein Timeout nach 20 Sekunden oder eine abgebrochene Verbindung ist keine Ablehnung. Der Eintrag bleibt mit „Desktop did not confirm this message“ in der Warteschlange, und noch wird nichts erneut gesendet.

  • Outbox-Prüfungrun.chatOutboxLoad

    Läuft, sobald die Verbindung nutzbar wird, oder alle 10 bis 13 Sekunden, solange im offenen Chat eine Nachricht wartet. Ist der Eintrag aufgelistet, in der Timeline sichtbar oder schon angenommen, wird nichts erneut gesendet.

  • Derselbe Schlüsselrun.chatOutboxSubmit:<mutation ID>

    Ein erneutes Senden verwendet die ausstehende Mutation und den gespeicherten Eintrag wieder, also wiederholen sich Schlüssel und Parameter. Der Desktop hebt eine fertige Antwort 48 Stunden auf und gibt sie zurück, statt doppelt zu speichern.

Anhänge werden vor der Nachricht hochgeladen. Der Desktop gibt eine Upload-ID und eine Blockgröße zurück, das Telefon begrenzt Blöcke auf 1 MiB und kodiert sie in Base64, und die Aufrufe verwenden die Schlüssel <upload key>:begin, :chunk:<n>, :finish und :cancel. Der Upload-Schlüssel ist files.chatAttachmentUpload:<session>:, gefolgt von einem 64-Bit-FNV-1a-Hash aus Session, Projekt, Dateiname, MIME-Typ, Pfad und Größe, sodass ein erneuter Versuch mit derselben Datei dieselben Schlüssel verwendet. Dateien dürfen bis zu 1 GiB groß sein, und ein PNG wird als JPEG mit Qualität 0,88 neu kodiert, wenn sein dekodiertes Bild höchstens 64 MiB braucht.

Das Warteschlangen-Sheet und das Zurückhalten durch den Desktop

Das Warteschlangen-Sheet listet die Einträge des offenen Chats, die in der Warteschlange stehen, Text oder Anhänge haben und zum Thread auf dem Bildschirm gehören. Ein Eintrag, der gerade gesendet wird, steht nicht im Sheet. Jede Zeile zeigt den Status, die Beschriftungszeile einer Nachricht aus einer anderen Session in den Wörtern des Desktops, den Text, Modell und Reasoning-Effort einer gespeicherten Auswahl, die Anhänge, den Grund des Zurückhaltens und den Fehler.

AktionWas das Telefon tut
Am Griff ziehenOrdnet zuerst die lokalen Einträge um und sendet dann die vollständige Reihenfolge der Queue-IDs mit run.chatOutboxReorder.
Sofort sendenSchiebt den Eintrag nach vorn, markiert ihn als sendend und sendet run.chatOutboxSubmit mit der Absicht automatic.
Bearbeiten, dann speichernSendet run.chatOutboxSubmit für einen neuen Eintrag am selben Platz der Warteschlange. Der Eintrag hat eine neue Operations-ID und die Absicht enqueue, und die Anfrage nennt die alte Operation als ersetzt.
Bearbeiten, dann sendenErzeugt denselben neuen Eintrag, schiebt ihn nach vorn, markiert ihn als sendend und sendet run.chatOutboxSubmit mit der Absicht automatic.
DeleteHält ein ausstehendes Entfernen mit eigener Mutations-ID fest und sendet es. Das Entfernen gilt, wenn der Desktop updated antwortet und den Eintrag nicht mehr listet. Die Methode ist run.chatOutboxRemove.

Die drei Mutationen brauchen einen Idempotenzschlüssel, und run.chatOutboxLoad liest die Warteschlange. Ein Ziehen endet von selbst nach 8 Sekunden, und 2 Sekunden nachdem es die Liste verlassen hat. Ändern sich die Einträge unter einem Ziehen, wird es abgebrochen.

Der Desktop kann einen Eintrag zurückhalten und den Grund in holdReason nennen. Das Telefon setzt dieses Feld nie und behandelt es nicht als Fehler. Die Zeile zeigt den Grund als zweite Beschriftung unter dem Text. Sofort senden ist ein Submit mit dem Intent automatic, und der Desktop wertet jeden Intent außer enqueue als Ihre Freigabe einer zurückgehaltenen Nachricht aus einer anderen Session. Eine Bearbeitung entfernt die Angabe message und den Grund des Zurückhaltens. Der neue Eintrag hat eine neue Operations-ID, und sein Prompt-Text ist der bearbeitete Text, sodass der Agent ihn als Ihre eigene Nachricht bekommt.

Eine angenommene Steuerung und „Stop and send now“

Eine angenommene Codex-Steuerung wartet auf den laufenden Schritt, und ein Tippen macht daraus einen eigenen Turn
Eine angenommene Codex-Steuerung wartet auf den laufenden Schritt, und ein Tippen macht daraus einen eigenen TurnEine Codex-Session im Zeitverlauf, nicht maßstäblich, mit dem Telefon oben, dem Desktop in der Mitte und dem laufenden Turn unten. Sie senden eine Nachricht, während ein Run aktiv ist. Das Telefon reicht sie als Steuerung für diesen Run ein, der laufende Turn nimmt sie an, und der Desktop markiert den Eintrag als angenommen, mit Run, Thread und Turn. Die Nutzerzeile zeigt dann „Sends after the current step“ mit der Aktion „Stop and send now“, weil Codex die Steuerung erst anwendet, wenn der laufende Schritt endet. Sie tippen auf die Aktion. Das Telefon sendet eine Anfrage mit Schlüssel, die den Eintrag und den angenommenen Run, Thread und Turn nennt, und blendet die Aktion aus. Der Desktop stoppt den Run und spielt die Nachricht als eigenen Turn erneut ab, und der Eintrag verlässt die Warteschlange. Eine gescheiterte oder blockierte Anfrage zeigt „Not confirmed“ ohne die Aktion.
Sie senden, während ein Run aktiv ist
Sie tippen auf „Stop and send now“
TelefonNutzerzeile
DesktopOutbox
Codex-Turnam Desktop
laufender Schritt: Modell und Tool-Aufrufe, kann Minuten dauern
ein eigener Turn
Steuerung für den aktiven Run
der Turn nimmt die Steuerung an
angenommen: Run, Thread, Turn
„Sends after the current step“
eine Anfrage mit Schlüssel, mit Run, Thread und Turn
Aktion ausgeblendet
stoppt den Run, spielt dann die Nachricht erneut ab
der Eintrag verlässt die Warte­schlange
Ein Fehler oder das Ergebnis blocked zeigt „Not confirmed“ ohne die Aktion. Die Steuerung bleibt, wo sie ist.
  • Angenommene Steuerungaccepted: runId · threadId · turnId · acceptedAt

    Nur der Desktop setzt es, wenn der laufende Turn die Steuerung angenommen hat. Es beweist nicht, dass die Nachricht im Verlauf steht.

  • Beschriftung und AktionWorkspaceChatUserOutboxDelivery

    Wird für einen Eintrag gezeigt, der gesendet wird, keinen Fehler und keine Übergabe hat und dessen angenommener Run der aktive Run ist. In einer Subagent-Ansicht muss der angenommene Thread dieser Thread sein.

  • Anfragerun.chatOutboxStopAndSendNow

    Trägt sessionId, queueId, operationId, expectedRunId, expectedThreadId und expectedTurnId. Ein Tippen erzeugt eine Mutations-ID für den Schlüssel, und der Aufruf wartet bis zu 60 Sekunden. Der Desktop prüft den genauen Turn.

  • Ergebnisstopping · notConfirmed

    Solange die Anfrage läuft, ist die Aktion ausgeblendet. Ein Fehler oder das Ergebnis blocked zeigt „Not confirmed“ ohne die Aktion, und die Steuerung bleibt, wo sie ist.

Eine Nachricht, die Sie während eines Runs senden, ist eine Steuerung für den aktiven Run, in einer Codex-Session und in einer Claude-Session. Die Abbildung zeigt eine Codex-Session. Der Desktop-Snapshot der Outbox besitzt den Zustand „angenommen“, die lokale Wiederholungsmenge nicht. Das View-Model behält die letzten Desktop-Einträge jeder Session und sucht den Eintrag über die Operations-ID, wenn es die ausstehende Nutzerzeile baut.

Die Aktion wird nur angeboten, solange der angenommene Run der aktive Run ist. Ein Wechsel des aktiven Runs veröffentlicht eine neue Outbox-Revision, wenn ein angenommener Eintrag den alten oder den neuen Run nennt, und so verschwinden Beschriftung und Aktion aus der Zeile. Das Telefon verfolgt den aktiven Turn nicht. Es sendet die Turn-ID, die der Desktop ihm gegeben hat, und der Desktop vergleicht sie mit dem Turn, der wirklich läuft.

Eine Anfrage, der eine ihrer sechs Identitäten fehlt, wird auf dem Telefon abgewiesen, bevor etwas gesendet wird. Die Phase einer Operation bleibt im Speicher. Nach „Not confirmed“ bietet die Zeile keinen zweiten Versuch für diese Operation an. Nach einem Fehler lädt das Telefon außerdem die Desktop-Outbox neu.

Nachrichten aus anderen Sessions

Eine Nachricht zwischen Sessions erscheint auf dem Telefon an drei Stellen, nie mit ihrem Umschlag
Eine Nachricht zwischen Sessions erscheint auf dem Telefon an drei Stellen, nie mit ihrem UmschlagEin Beispiel mit zwei Sessions auf einem Desktop: „API review“, eine Claude-Session, sendet eine Nachricht an „Fix tests“, eine Codex-Session. In der Timeline von „API review“ ist die Nachricht ein Band mit der Beschriftungszeile „This session → Fix tests“. Der Desktop stellt sie für „Fix tests“ in die Warteschlange, wo das Warteschlangen-Sheet eine Zeile mit der Beschriftungszeile „API review → This session“, dem Nachrichtentext und dem Grund zeigt, warum der Desktop sie zurückhält. Nach der Zustellung zeigt die Timeline von „Fix tests“ eine User-Zeile mit der Beschriftungszeile „API review → This session“. Ein Tippen auf den Namen des anderen Endes öffnet dieses Ende an der Zeile der Nachricht. Der Prompt-Text mit dem Umschlag für den Agenten steht im Outbox-Eintrag, aber keine Ansicht zeigt ihn.
API reviewClaude, der Absender
Fix testsCodex, der Empfänger
Chat-TimelineThis session → Fix testsRun the auth tests again.
der Desktop reiht sie für die andere Session ein
Warteschlangen-SheetAPI review → This sessionRun the auth tests again.„Waiting for your approval. …“
zugestellt, wenn nichts sie zurückhält
Chat-TimelineAPI review → This sessionRun the auth tests again.
Tippen auf „API review“
„API review“ öffnet an der Zeile
Der Outbox-Eintrag hat auch promptText mit dem Umschlag für den Agent. Das Telefon dekodiert ihn und zeigt ihn in keiner Ansicht.
  • Angabe messagemessage: direction · from · to · peer · delivery · link

    Der Desktop erzeugt jedes Wort der Zeile. Das Telefon zeigt die Wörter und erzeugt keine. Eine Angabe mit einem Wert, den die App nicht kennt, lässt das Dekodieren scheitern.

  • Zeile der WarteschlangeuserText · message · holdReason

    Der Zeilentext ist userText, nie promptText. Die Beschriftungszeile kommt aus der Angabe message des Eintrags, und der Grund des Zurückhaltens ist eine zweite Beschriftung, die nur der Desktop setzt.

  • Link zur Gegenstellemessage.link · anchor

    Ein Tippen auf den Namen des anderen Endes lädt dessen Timeline mit dem Anker. Die Zeile der Nachricht erscheint in der Mitte mit einer Markierung. Der Weg zurück ist der Link auf dieser Zeile.

Ein Agent in einer Session kann eine Nachricht an eine andere Session desselben Desktops senden, und Agenten in einer Session senden einander Nachrichten. Das Telefon kann eine solche Nachricht nicht erzeugen. Es zeigt die Angabe message, die der Desktop an die Zeile hängt. WorkspaceChatMessageBand zeigt jede Nachrichtenzeile: die Beschriftungszeile mit den beiden Enden, zum Beispiel „This session → Fix tests“ oder „API review → This session“, das Signal Sent, Needs approval oder Not sent und den Text als Markdown. Der Desktop entfernt den Umschlag, daher ist der Zeilentext nur die Nachricht.

Der Decoder ist streng. Eine Angabe message mit einem Wert, den dieser Build nicht kennt, oder mit einem leeren Wort lässt das Dekodieren scheitern. Das Telefon erzeugt keine eigenen Wörter: Der Zeilentitel, die beiden Enden und „Untitled session“ kommen vom Desktop. Die Leitung trägt für eine Nachrichtenzeile kein detail. Ein Tippen auf das Band öffnet das Nachrichtendetail, das das Telefon aus dem Zeilentitel, der Angabe und dem Text baut. Das Detail behält die Zeilenumbrüche des Absenders.

Wenn die Angabe einen Link hat und der Chat im Workspace ist, ist der Name des anderen Endes in der Beschriftungszeile der Link. Er hat die Linkfarbe, das Agentenzeichen des anderen Endes und eine Tippfläche von mindestens 44 Punkten. VoiceOver öffnet mit der Standardaktion das Detail, und die benannte Aktion „Go to <peer>“ folgt dem Link. WorkspaceMessageJumpModel zeigt die Session und den Thread des anderen Endes. Der Chat dieser Stelle nimmt den Anker einmal und lädt das Fenster um die Zeile. Die Collection-View setzt die Zeile in die Mitte des Bildschirms und markiert sie. Sie hält die Zeile in jedem Layout-Durchlauf in der Mitte, bis Sie ziehen, auf die Statusleiste tippen oder der Chat einen anderen Scroll-Befehl bekommt. Es gibt kein Zurück-Bedienelement: Der Weg zurück ist der Link auf der Zeile am anderen Ende. Im Nachrichtendetail schließt derselbe Link zuerst das Detail und folgt dann dem Link.

Ein Fenster mit einem newerCursor ist eine Seite in der Vergangenheit. Solange eine solche Seite sichtbar ist, lädt jedes Neuladen der Timeline dieselbe Seite erneut mit einem Zeilenanker an ihrer Zeile, auch nach einem Abgleich, einer Lücke im Relay oder dem Ende eines Runs. Die Aufdeckung bleibt, daher bewegen sich die Zeilen nicht, und keine Zeile bekommt eine neue Markierung. Das Telefon übernimmt das activeRun mit dem Wert null einer Seite in der Vergangenheit nicht, daher bleibt Stop sichtbar, solange ein Run aktiv ist. Ein Hinweis des Desktops, zum Beispiel „This message is not in the saved history.“, erscheint über der Timeline.

Tests, die dieses Verhalten festhalten

  • WorkspaceChatTimelineSnapshotOrderingTests: Nur die aktive Operation darf ihren Snapshot anwenden, und eine geänderte Zielgeneration weist eine frühere ab.
  • WorkspaceChatTimelinePaginationReloadTests: Ein Wiederherstellungs-Neuladen behält die Zeilen, bis sein Snapshot angewendet ist, und ein ausstehendes Neuladen übersteht einen vorübergehenden Verbindungsverlust.
  • WorkspaceChatLiveTimelineConfirmationTests: Eine Live-Zeile wird nur über eine gemeinsame typisierte Identität abgeschlossen, nie über übereinstimmenden Text oder Zeitstempel.
  • WorkspaceChatMessageLinkRoundTripTests: Jeder Link landet genau auf der Zeile in der Mitte einer Seite in der Vergangenheit, zwischen zwei Sessions und zwischen der Haupt-Timeline und einem Agent-Thread.

Diese Suiten liegen in VibeUITests, das in der App gehostet läuft, deshalb brauchen sie ein iOS-Simulator-Ziel. Verhalten auf echten Geräten, etwa die erste Anzeige mit gespeichertem Markdown-Verlauf, wird weiterhin von Hand geprüft.