iOS: Pakete, Start und die Relay-Verbindung
Wie die iPhone-App in Swift-Pakete aufgeteilt ist, warum der Start auf den Schlüsselbund wartet, wie ihr Network.framework-Socket sich registriert, Zeitlimits einhält, sich erholt und Live-Events filtert, und wie sie Agent-Limits liest.
Gegen den Quellcode geprüft am 5. Oktober 2026
Auf dieser Seite
Drei Swift-Pakete und ein schlankes App-Target
Das Xcode-Projekt hat zwei Targets: die HBCode-App und ein VibeUITests-Bundle, das in ihr gehostet läuft. Das App-Target enthält drei Swift-Dateien: den SwiftUI-Einstiegspunkt, den App-Delegate und eine Delegate-Erweiterung für das Timeline-Interesse. Es bindet Core und VibeUI direkt ein, und sein Delegate ruft Core für Push, den Hintergrund und die Wiederherstellung auf. Alles andere liegt in drei lokalen Swift-Paketen, gebaut mit Swift 6.3 für iOS 18.
| Target oder Paket | Was es enthält | Hängt ab von |
|---|---|---|
| App-Target | Drei Swift-Dateien: der Swift | Core und Vibe |
| Vibe | Jeder Bildschirm, die UIKit-Chat-Collection, die Code- und Dokumentbetrachter, der Review Mode und die gebündelte mermaid. | Core, Vibe |
| Core | Ein Library-Target aus Sources/ | Keychain |
| Vibe | Markdown-Texthelfer: der Pfadklassifizierer, der Linkifier für Inline-Referenzen und der Normalisierer für nummerierte Listen. | Nichts. |
| Vibe | Das Test-Bundle, gehostet in der App. | Core, Vibe |
Core kann VibeUI nicht importieren, deshalb bauen und testen Relay-Client, Anmeldung und Datendienste ohne einen einzigen Bildschirm.
| Pfad vom Repository-Stammverzeichnis | Beginnen Sie hier für |
|---|---|
| mobile/ | Start, Push-Registrierung und die Übergänge zwischen Hintergrund und Vordergrund. |
| mobile/ | Der Relay-Socket, die Registrierung, Wiederherstellungsepochen und das Timeline-Interesse-Lease. |
| mobile/ | Annahme von Relay-Events vor dem Main Actor. |
| mobile/ | Chat-Hosts, die Collection-View, die Outbox und Chat-Vorschauen. |
| mobile/ | Aufnahme, Transkription und Videokomposition im Review Mode. |
Der Start wartet auf den Schlüsselbund
- Geräte-IDAfterFirstUnlockThisDeviceOnly
Die Identität des Telefons für die Geräteliste und das Relay. Sie wird nie mit iCloud synchronisiert. Die Plattform hält diese Klasse lesbar, während das Telefon gesperrt ist, aber nicht vor dem ersten Entsperren nach einem Neustart.
- App-JWT und Resume-TokensWhenUnlockedThisDeviceOnly
Der Standard für die Schlüsselbundeinträge der App. Die Plattform macht diese Klasse nur lesbar, solange das Telefon entsperrt ist.
- StartprotectedDataDidBecomeAvailable
Der App-Delegate und AppView lesen die Geräte-ID abseits des Main Threads. Solange sie nicht lesbar ist, startet Core nicht, und AppView behält seinen Ladebildschirm. Beide versuchen es erneut, wenn geschützte Daten verfügbar werden oder die App in den Vordergrund zurückkehrt.
- AnmeldeprüfungcheckStoredToken
Ist das App-JWT nicht lesbar, weil das Telefon gesperrt ist, bleibt die gespeicherte Anmeldung erhalten. Die Prüfung läuft erneut, wenn geschützte Daten verfügbar werden oder die App aktiv wird.
Die Geräte-ID ist die Identität des Telefons für die Geräteliste und das Relay, deshalb startet nichts, was mit dem Netzwerk spricht, ohne sie. Sie ist ein Schlüsselbundeintrag mit der Zugänglichkeit AfterFirstUnlockThisDeviceOnly, der nie mit iCloud synchronisiert wird. Das App-JWT, sein Ablaufzeitpunkt und die Relay-Resume-Tokens verwenden den Standard für die Einträge der App, WhenUnlockedThisDeviceOnly.
Der App-Delegate liest die Geräte-ID in einer Hintergrundaufgabe, bevor er Core initialisiert, und AppView löst die Identität ebenfalls abseits des Main Threads auf. Bei einem Start im Hintergrund vor dem ersten Entsperren bleibt AppView auf dem Ladebildschirm und versucht es erneut, sobald das System protectedDataDidBecomeAvailable meldet. Während das Telefon gesperrt ist, hält die Plattform die Geräte-ID lesbar, das App-JWT aber nicht, deshalb behält ein Start in diesem Zustand die gespeicherte Anmeldung und prüft sie erneut, sobald geschützte Daten verfügbar sind oder die App aktiv wird. Ein Eintrag, der sich gar nicht lesen lässt, bietet Reset Device Identity an.
Die Anmeldung braucht keine Callback-URL. ASWebAuthenticationSession öffnet /auth/auth0/initiate-login mit callbackURLScheme auf nil, und die App fragt /auth0/poll-status alle 2 Sekunden ab, bis zu 60 Mal, bevor sie den Code mit ihrem PKCE-Verifier eintauscht und die Anmeldung abschließt. Es läuft immer nur eine Erneuerung des App-JWT. Ihr Timer feuert 300 Sekunden vor dem Ablauf, frühestens aber in 5 Sekunden, und ein 401 oder 403 vom Erneuerungsendpunkt beendet die Anmeldung.
Die gewählte Region wird in UserDefaults gespeichert. Ihre Basis-URL, https://api-us.hbcode.ai oder https://api-eu.hbcode.ai, bestimmt sowohl den REST-Client als auch den /ws/device-link-Socket. iOS speichert die gewählte Region, bevor es den Navigationszustand veröffentlicht, und bindet jeden Registrierungsversuch an den Ursprung, den er verwendet hat.
Ein WebSocket auf Network.framework
Der Relay-Socket ist eine NWConnection mit NWProtocolWebSocket: Version 13, automatische Antworten auf Pings und 32 MiB maximale Nachrichtengröße. Die Parameter erlauben teure und eingeschränkte Pfade und verwenden den Multipath-Dienst handover, sodass die Verbindung zwischen WLAN und Mobilfunk wechseln kann. Nur wss-URLs werden akzeptiert. Die Upgrade-Anfrage trägt das App-JWT als Bearer-Token, X-Device-ID und X-Token-Binding mit der Geräte-ID, X-Client-Type: mobile und X-Target-Desktop-Device-ID für den gewählten Desktop.
Jeder Socket erhält eine Transportgeneration. Das Zustandsobjekt, das einen Socket installiert, führt unter derselben Sperre auch Sendungen aus, die an eine Generation gebunden sind, deshalb kann eine für einen früheren Socket vorbereitete Sendung nie dessen Nachfolger erreichen. Zustands-Callbacks einer Verbindung, die nicht mehr aktuell ist, werden ignoriert.
Nach dem Upgrade sendet der Client register mit der Geräte-ID, dem Gerätenamen, relayProtocolVersion 1.7, dem Zieldesktop sowie der Relay-Session-ID und dem Resume-Token, wenn er sie hat. Der Handshake läuft nach 10 Sekunden ab, und eine Sendung, die aus einem vorübergehenden Grund scheitert, wird nach min(0.25 × attempt, 1.5) Sekunden wiederholt. Nach der Registrierung sendet der Client alle 5 Sekunden eine eigene heartbeat-Nachricht, und ein Watchdog lässt die Verbindung nach 15 Sekunden ohne eingehenden Verkehr scheitern.
Resume-Tokens liegen pro Paar aus Telefon und Desktop im Schlüsselbund, unter dem Schlüssel <device ID>::<desktop ID>. Ein serieller Speicher gibt jedem Relay-Client eine Besitzgeneration für seinen Schlüssel und weist Schreibvorgänge eines Clients ab, dem er nicht mehr gehört, sodass ein veralteter Client kein neueres Token überschreiben kann. Ein invalidResume-Fehler lässt den Client sich nach 0,2 Sekunden ohne Token neu registrieren. Drei ungültige Resumes innerhalb von rollierenden 30 Sekunden beenden die Schleife mit invalidResumeLoop.
Anfragen, Zeitlimits und Trace-Kontext
RpcMethod listet 69 Methoden: 10 im Namensraum system, 23 für Dateien, 15 für Sessions und 21 für Runs. 24 davon sind Mutationen, die einen Idempotenzschlüssel verlangen. Der Client weist eine unbekannte Methode oder eine Mutation ohne Schlüssel ab, bevor etwas gesendet wird, und der ausgehende Encoder wirft einen Fehler, sobald er irgendwo in einer Payload einen snake_case-Schlüssel findet. Ausstehende Aufrufe sind nach Anfrage-ID zusammen mit einem Besitz-Token geordnet. Eine zweite laufende Anfrage mit derselben ID wird abgewiesen, und das Aufräumen eines alten Aufrufs kann keinen neueren entfernen.
| Standard-Zeitlimit | Methoden |
|---|---|
| 8 Sekunden | files. |
| 90 Sekunden | Dateibäume, Run-Liste und -Abruf, Codex-Nutzungslimits und -Auth-Profile, Claude-Konten, der Wechsel des Codex-Profils und des Claude-Kontos, Nutzungsverlauf der Agents, Sprachvokabular und -kontext sowie Verlaufszustand. |
| 60 Sekunden | Session- und Projektlisten, Abruf und Aktualisierung einer Session, Reihenfolge von Projekten und Sessions, Wiederherstellung fehlender Workspaces, Abgleich, Projekt-Skills, Workspaces, bestehende Sessions, Abbrechen und Löschen, Diffs, Befehlsausgabe, Webseiten als Markdown, Binär- und Medienlesevorgänge sowie Anhang-Uploads. |
| 30 Sekunden | Jede andere Methode. |
| 115, 60 und 20 Sekunden | Chat-Richtlinie: die erste Timeline, eine ältere Seite und die Outbox-Bestätigung. |
| 60 Sekunden | Chat-Richtlinie: run. |
Jede Anfrage trägt einen W3C-traceparent, und eine Antwort wird abgewiesen, wenn ihre Trace-ID nicht zu der der Anfrage passt. Ein Zeitlimit lässt diesen Aufruf scheitern und verbindet die Socket-Generation neu, die ihn gesendet hat. Wurde dieser Socket bereits ersetzt, berührt das Zeitlimit nichts anderes.
Wiederherstellungsepochen, Netzwerkwechsel und der Hintergrund
- Epoche der WiederherstellungdesktopReconnectGraceSeconds = 90
Eine pro Desktop. Sie endet zur reconnectDeadline, die das Relay geschickt hat, oder ohne sie 90 Sekunden, nachdem das Telefon den Verlust bemerkt hat. Stille, ein RPC-Zeitlimit oder ein verlorener Pfad innerhalb der Epoche behalten diese Frist.
- Prüfungsystem.ping
Vor jedem erneuten Versuch wartet das Telefon eine zufällige Zeit zwischen der Hälfte und dem Ganzen einer Obergrenze, die sich ab 0,25 s verdoppelt und bei 5 s endet, nie über die Frist hinaus. Ein Versuch ohne Socket öffnet zuerst einen, und dieser registriert sich mit seinem Resume-Token.
- SchonfristDESKTOP_RECONNECT_GRACE_SECS = 90
Schließt sich der Socket eines Desktops, sendet das Relay reconnecting mit einer reconnectDeadline und beantwortet Anfragen an diesen Desktop mit -32011. Es sendet online, wenn sich der Desktop erneut registriert, und disconnected, wenn die Frist verstreicht.
- ErgebniscanSendDesktopRPC
Der Desktop gilt erst als verbunden, wenn er selbst auf system.ping antwortet. Das Telefon zeigt ihn als offline an, wenn die Frist verstreicht, sofort bei disconnected oder wenn die Prüfung mangels Netzwerkpfad endet.
Die Wiederherstellung läuft in Epochen, eine pro Desktop, jede mit einer Frist. Innerhalb einer Epoche prüft die App den Desktop mit system.ping und Backoff, und sendet das Relay online, pingt sie sofort. Ein Desktop, der bis zur Frist nicht geantwortet hat oder den das Relay als disconnected meldet, wird als vorübergehend offline angezeigt. Ebenso einer, dessen Prüfung endet, weil das Telefon keinen Netzwerkpfad hat.
NWPathMonitor meldet verlorene Pfade und Schnittstellenwechsel. Ein verlorener Pfad schließt jedes verbundene Relay, lässt dessen ausstehende Aufrufe scheitern und öffnet eine Epoche. Ein Wechsel zwischen WLAN und Mobilfunk schließt das alte Relay und lässt laufende Aufrufe scheitern, bevor die Wiederherstellung genau einen Ersatz öffnet, denn ein migrierter Socket kann weiter Sendungen annehmen, ohne je auf eine Registrierung zu antworten. Der Wechsel verwirft die aktuelle Epoche, deshalb bekommt der Ersatz ein neues Fenster von 90 Sekunden.
Wird die App inaktiv, setzt sie das Timeline-Interesse aus. Geht sie in den Hintergrund, trennt sie jedes Relay innerhalb einer GracefulDisconnect-Hintergrundaufgabe, außer desktopgestützte Medien spielen, was der Audio-Hintergrundmodus erlaubt. Zurück im Vordergrund startet die Wiederherstellung für den aktiven Desktop 1,5 Sekunden später, und ein bestehender Socket bleibt nur, wenn ein Ping innerhalb von 3 Sekunden antwortet. Ein stiller desktop_online-Push startet dieselbe Wiederherstellung sofort.
Event-Annahme vor dem Main Actor
- Snapshot-Platzchat:timeline-updated · event: blocks
Ein Warteplatz pro Session, Run und Thread, und ein neuerer Snapshot ersetzt den wartenden. Über 128 Plätze hinaus wird der älteste verworfen. Snapshots gehen einzeln hinaus, mit 100 ms Pause nach jeder Zustellung.
- Steuer-Eventsrun:status-changed · device-status
Jedes andere Relay-Event wartet in Ankunftsreihenfolge und wird ohne Pause zugestellt, deshalb kann es einen wartenden Snapshot überholen.
- Lease-PrüfungWorkspaceTimelineRelayEventAuthorization
Läuft auf dem Main Actor, wenn ein Timeline-Event zugestellt wird, und verwirft das Event vor dem Dekodieren, wenn sein Desktop und seine Session keine aktuelle Lease halten. Die Prüfung läuft erneut, direkt bevor sich das View-Model ändert.
- Überlastung32 MiB · 64 MiB · 256 Events
Ein Event über 32 MiB, mehr als 64 MiB wartend oder mehr als 256 wartende Steuer-Events verwerfen den Rest dieser Abonnementgeneration. Das Telefon abonniert dann neu und startet die Wiederherstellung des Desktops.
RelayEventIngress nimmt Relay-Events an, bevor irgendeine Arbeit den Main Actor erreicht. Steuer-Events behalten ihre Ankunftsreihenfolge und warten nie hinter Snapshots. Block-Snapshots aus chat:timeline-updated sind UI-Zustand nach bestem Bemühen. Ein neuerer Snapshot für dieselbe Session, denselben Run und Thread ersetzt einen noch wartenden, und Snapshots gehen einzeln hinaus. Die Lease wird geprüft, wenn ein Snapshot auf dem Main Actor zugestellt wird, bevor seine Payload dekodiert ist, deshalb erreichen Inhalte eines Chats, den Sie verlassen haben, nie die Aufbereitung.
Timeline-Inhalte erreichen das Telefon nur, solange ein Chat sichtbar ist. Jede sichtbare Chat-Ansicht hält einen Anspruch, und der Interesse-Koordinator hält eine Lease pro Desktop, für die Basis-Session des neuesten Anspruchs. Ansprüche halten die Übergabe zwischen einem Eltern-Chat und einem Subagent-Viewer sicher, solange beide Ansichten kurz nebeneinander existieren, und ein Anspruch zählt nur für die Vordergrundgeneration, in der seine Ansicht zuletzt ihre Sichtbarkeit bestätigt hat, sodass die Rückkehr in den Vordergrund keine verschwundene Ansicht wiederbeleben kann. Die Lease wird als chat:timeline-interest mit der appSessionId und einem active-Flag gesendet, alle 20 Sekunden erneuert und nach der Registrierung beim Relay erneut ausgestellt. Eine gescheiterte Zustellung wird bis zu 4 Mal versucht, beginnend bei 0,25 Sekunden, mit höchstens 2 Sekunden zwischen den Versuchen und 20 Prozent Jitter.
| Relay-Ereignis | Geht an |
|---|---|
| device-status, device-unlinked | Die Desktop-Präsenz mit der Wiederherstellungsepoche und das Entfernen eines Desktops, dessen Verknüpfung aufgehoben wurde. |
| project-directory-updated | Das Projektverzeichnis dieses Desktops. |
| session-created, session-updated, session-deleted, session-snapshot, history-state-changed | Der Session-Dienst. |
| chat:timeline-updated | Der sichtbare Chat, nach der Lease-Prüfung. Dasselbe Event aktualisiert auch die Run-Zusammenfassungen. |
| chat:outbox-updated | Eine Benachrichtigung chat |
| run:created, run:deleted, run:list-invalidated, run:status-changed, run:tokens-updated, run:cost-updated, run:finalized, run:stream-progress, run:error-details | Die Run-Zusammenfassungen in Jobs |
Die Aktivitätsanzeige einer Session stammt aus den Run-Zusammenfassungen. Ein Event chat:timeline-updated mit dem Event started oder blocks markiert seinen Run als laufend. Die Events completed, failed und aborted markieren ihn als abgeschlossen, gescheitert und abgebrochen und setzen die Endzeit. Ein Event ohne Run-Metadaten wird mit einer Warnung verworfen, und eines ohne Run-ID ändert nichts.
Agent-Limits und Kontowechsel
- Codex-Abfragensystem.codexUsageLimits · system.codexAuthProfiles
Ein vollständiges Laden sendet beide parallel. Eine Profilliste ohne aktives Profil bedeutet „Not set up“, und die gescheiterte Limit-Abfrage bleibt verborgen.
- Claude-Abfragesystem.claudeAccounts
Eine Abfrage liefert jedes Konto mit seinen Nutzungsfenstern. Eine gescheiterte Abfrage behält die letzten Konten, und die Konten eines anderen Desktops werden vor dem Laden geleert.
- AktualisierunglimitRefreshIntervalSeconds = 60
Läuft, solange die Projektliste auf dem Bildschirm ist. Codex liest nur die Limits erneut. Claude liest die Kontoliste erneut.
- Wechselsystem.activateCodexAuthProfile · system.activateClaudeAccount
Eine Anfrage mit Schlüssel pro Tippen, mit einer neuen UUID im Schlüssel. Es läuft immer nur ein Wechsel. Die Antwort ist die neue Liste. Codex lädt danach neu, weil die Limits zum aktiven Abonnement gehören.
Die Projektliste zeigt eine Kachel pro Agent, und die Einstellungen haben einen Abschnitt Agents mit denselben zwei Statuszeilen. Ein Tippen öffnet den Nutzungsbildschirm dieses Agents. Konten und Profile liegen auf dem Desktop. Das Telefon liest sie über das Relay und kann nur das aktive auswählen. Ein Konto fügen Sie auf dem Desktop hinzu, und dort melden Sie sich auch an.
| Text der Kachel | Wann die Codex-Kachel ihn zeigt | Wann die Claude-Kachel ihn zeigt |
|---|---|---|
| Checking… | Ein Laden läuft, und keine Limits sind bekannt. | Die erste Abfrage dieses Desktops hat noch nicht geantwortet. |
| Not set up | Die Profilliste hat kein aktives Profil, und die Limits werden geleert. | Die Kontoliste ist leer, und die Abfrage ist nicht gescheitert. |
| Unavailable | Nach dem Laden ist kein Limit-Fenster bekannt. | Die Kontoliste ist leer, und die Abfrage ist gescheitert. |
| Abgemeldet | Die Codex-Kachel zeigt ihn nie. | Kein Konto ist aktiv, oder das aktive Konto ist abgemeldet. |
| Nutzungszeilen | Das primäre und das sekundäre Fenster des aktiven Profils, benannt nach ihrer Länge, 5h und Weekly. | Das 5h-Fenster und das Wochenfenster aller Modelle des aktiven Kontos. |
| Could not read usage | Die Codex-Kachel zeigt ihn nie. | Das aktive Konto hat keine Nutzungsfenster und eine Fehlermeldung. |
| Name des Plans oder No plan limits | Die Codex-Kachel zeigt ihn nie. | Das aktive Konto hat keine Nutzungsfenster und keinen Fehler. |
Eine Nutzungszeile zeigt den Prozentsatz, der übrig ist: 100 minus usedPercent, begrenzt auf 0 bis 100. Die Zeile wechselt bei 25 Prozent oder weniger in die Warnfarbe und bei 10 Prozent oder weniger in die Fehlerfarbe. Der Claude-Nutzungsbildschirm listet auch die Wochenfenster einzelner Modelle, die die Kachel weglässt.
Auf dem Codex-Nutzungsbildschirm sendet ein Tippen auf ein Profil, das authentifiziert und nicht aktiv ist, system.activateCodexAuthProfile. Auf dem Claude-Nutzungsbildschirm sendet ein Tippen auf ein Konto, das angemeldet und nicht aktiv ist, system.activateClaudeAccount, und dieses Konto führt den nächsten Claude-Turn aus. Beide Aufrufe sind Mutationen mit Schlüssel. Der Schlüssel besteht aus der Methode, der ID und einer neuen UUID, also ist jedes Tippen eine eigene Mutation. Ein View-Model führt immer nur einen Wechsel aus. Ein gescheiterter Codex-Wechsel setzt den Ladezustand auf gescheitert, und die Kachel behält die Limits, die sie schon hat. Ein gescheiterter Claude-Wechsel behält die Konten und speichert den Fehler.
Jeder Nutzungsbildschirm liest außerdem system.agentUsageHistory für seinen Agent. Die Wochenlinie entsteht aus weeklySamples. Liegt ein Reset-Zeitpunkt zwischen zwei Messwerten und ist der spätere Messwert niedriger, bekommt die Linie zwei zusätzliche Punkte: den alten Wert eine Sekunde vor dem Reset und null beim Reset. Das Token-Diagramm umfasst immer die letzten 30 lokalen Tage und existiert nur für Codex, weil dailyTokens für Claude null ist. Eine gescheiterte Abfrage behält den Verlauf, der schon auf dem Bildschirm ist.