Android: Projektionen, Batch-Abgleich und Verwahrung
Wie die Android-App Timeline-Projektionen zwischenspeichert, Anfragen mit einer Sichtbarkeitsgeneration umzäunt, bis zu 16 Timelines pro Anfrage abgleicht, ihre LazyColumn verankert hält und jede Sendung verschlüsselt verwahrt.
Gegen den Quellcode geprüft am 17. September 2026
Auf dieser Seite
Ein Cache für Timeline-Projektionen
Ein TimelineTarget besteht aus einer App-Session, einem Projektordner, einem optionalen Codex-Thread 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.
Bis zu 16 Ziele pro Anfrage, bei Fehlern halbiert
- Batchrun.codexChatReconcileSessions
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.
- HalbierungisolateBatchFailure
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 SeiteTIMELINE_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).
- TorWorkspaceTimelineSyncGate
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 werden in Batches von bis zu 16 Zielen pro Projektordner über run.codexChatReconcileSessions abgearbeitet. Ziele, die den sichtbaren Bereich nicht mehr besitzen, werden vor dem Absenden abgebrochen. Die Antwort muss für jedes angefragte Ziel genau eine Session enthalten, zugeordnet über die exakte Identität.
Eine gescheiterte Anfrage, ein Dekodierfehler oder eine nicht passende Antwort halbiert den Batch 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 TIMELINE_SNAPSHOT_TOO_LARGE-Fehler halbiert den Batch ebenso, und ein einzelnes Ziel versucht es dann bis zu 3 Mal mit der ersten Seitengröße von 12, nach 750 Millisekunden, verdoppelt auf 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.
Verschlüsselte Verwahrung jeder Sendung
- Verschlüsselte AbsichtSharedPreferencesWorkspaceOutboxStore
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.
- Quittungrun.codexChatOutboxSubmit
Ü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.
- Abgleichenrun.codexChatOutboxLoad
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-IDandroid-<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>. Der Idempotenzschlüssel für die Übermittlung, run.codexChatOutboxSubmit:<uuid>, wird neu erzeugt, wenn Sie einen Eintrag sofort senden oder bearbeiten, weil der Desktop die Zustellung über die unveränderlichen Queue- und Operations-IDs dedupliziert. Ü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.
Ist der Desktop wieder bereit, liest der Warte-Koordinator run.codexChatOutboxLoad 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.
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.
- PlanToCodeAndroidWorkspaceReconciliationCoordinatorTest: 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.
./gradlew :app:testDebugUnitTest
./gradlew :app:pixel2api35DebugAndroidTest