Relay-Wire-Protokoll
Die WebSocket-Envelopes, der Methodenvertrag für beide Agenten, die Versionsschranke, die Frist jeder Station, die Routing-Autorität und die Mutations-Replay-Regeln, die die Telefon-Apps verwenden.
Gegen den Quellcode geprüft am 5. Oktober 2026
Auf dieser Seite
Den kanonischen Methodenvertrag finden
- rpc-contract.v1.jsonmethods · mutationsRequiringIdempotency
Das Methodeninventar. Kein Programm lädt es, und kein Code wird daraus erzeugt. Die Prüfung liest es neben jeder Kopie und schlägt bei jeder Abweichung fehl.
- Relay-Listerequest_validation.rs
Wird vor dem Weiterleiten geprüft. Eine Methode, die nur hier fehlt, scheitert daher bei jedem Aufruf mit -32601, auch wenn beide Apps und der Desktop sie kennen. -32027 gilt nur für Verbindungen mit API-Schlüssel.
- Desktop-Routerrouter/mod.rs
Verlangt für die 24 gelisteten Mutationen einen nicht leeren Idempotenzschlüssel von höchstens 256 Bytes. Eine unbekannte Methode scheitert in ihrem Namespace-Handler.
- Telefon-EnumsRpcMethod · requiresIdempotencyKey
iOS und Android können keine Anfrage außerhalb des Enums und keine Mutation ohne Schlüssel bauen. Die Prüfung vergleicht ihre Methodennamen und ignoriert diese Flags.
Das Manifest führt 74 Methoden in vier Namespaces auf. 25 davon sind Mutationen, die einen Idempotenzschlüssel brauchen. Das Relay und der Desktop-Router halten jeweils dieselben zwei Listen im Code.
| Namespace | Beispiele |
|---|---|
| system, 13 Methoden | system. |
| session, 15 Methoden | session. |
| run, 23 Methoden | run. |
| files, 23 Methoden | files. |
Vier Methoden bedienen den Antwortstil, die Textverbesserung und die Kontextübersicht. Die ganze Arbeit dieser Methoden macht der Desktop.
| Methode | Parameter und Ergebnis |
|---|---|
| system. | Keine Parameter. Gibt die Einstellung des Antwortstils zurück: label, description, style und options. Jede Option hat value, label und description. Alle diese Wörter schreibt der Desktop, und die Clients zeigen sie unverändert. |
| system. | Mutation. Parameter: style, ste oder default. Speichert den einen Stil aller Agenten und gibt dieselbe Einstellung wie system. |
| run. | Parameter: app |
| run. | Parameter: app |
Eine neue Kontextmessung kommt im runtime-Event von chat:stream an.
Eine registrierte Verbindung sendet einen Umschlag aus type und payload. RegisterPayload verwendet deviceId, relayProtocolVersion und optional deviceName, sessionId, resumeToken und targetDesktopDeviceId. Die Geräte-UUID muss zu einer Gerätezeile passen, die das Konto über HTTP registriert hat, die nicht vergessen ist und zu Rolle und Relay-Berechtigung der Verbindung passt. Anfrage-Payloads weisen unbekannte Felder zurück, sodass ein Client, der eine userId in seine eigene Payload schreibt, den ganzen Frame scheitern lässt.
Das Manifest legt auch Größengrenzen fest, die der Desktop im Code durchsetzt.
| Grenze | Wert und Methode |
|---|---|
| Relay-Frame | 32 Mi |
| Sessions pro Aufruf | 16, run. |
| Größe des Ergebnisses | 8 Mi |
| Outbox-Einträge pro Session | 256, run. |
| Seite roher Befehlsausgabe | 256 Ki |
| Roher Binär-Lesevorgang | 16 Mi |
pnpm check:rpc-contractDie Versionsschranke sitzt bei der Registrierung
Die Relay-Protokollversion ist 1.7. Das Manifest nennt 1.7 als aktuelle und als minimale Version, und der Desktop, die iOS-App und die Android-App melden alle 1.7 in RegisterPayload. Version 1.7 gab den Chat-Methoden ihre agentenneutralen run.chat-Namen. Das Relay enthält keine Übersetzung für ein älteres Protokoll.
Bei der Registrierung berechnet das Relay die erforderliche Version als das Höhere aus 1.7 und der höchsten minimum_relay_protocol_version der Kompatibilitätsregeln in der Datenbank. Es hält die Regeln 60 Sekunden im Cache. Eine Verbindung unterhalb der erforderlichen Version wird abgewiesen, bevor sie eine Anfrage senden kann.
Die Registrierung scheitert mit einem von drei Fehlercodes.
- protocolUpdateRequired: relayProtocolVersion fehlt oder ist niedriger als die erforderliche Version. Die Meldung fordert den Benutzer auf, die App zu aktualisieren.
- invalidRelayProtocolVersion: relayProtocolVersion lässt sich nicht als Version lesen.
- relayProtocolPolicyUnavailable: Das Relay kann die Kompatibilitätsregeln nicht lesen, oder eine Regel enthält eine Version, die sich nicht lesen lässt.
Eine zweite Prüfung läuft über HTTP, bevor sich der Socket öffnet. POST /public/compatibility/check nimmt einen desktop-Deskriptor, einen mobile-Deskriptor oder beide entgegen, jeweils mit appVersion, platform und relayProtocolVersion. Die Antwort enthält compatible, decision, message, policyVersion, requiredRelayProtocolVersion und je Deskriptor ein Ergebnis mit knownVersion und protocolSupported. decision ist einer der Werte compatible, missingClient, invalidVersion, unsupportedDesktopVersion, unsupportedMobileVersion, unsupportedPair, protocolUpdateRequired und blockedByPolicy.
Der Desktop ruft diese Prüfung mit einem Timeout von 5 Sekunden auf, bevor er sich verbindet. Er sendet nur seinen eigenen Deskriptor. Die Prüfung lässt im Fehlerfall durch: Ist sie nicht erreichbar, liefert sie einen Fehlerstatus, einen nicht lesbaren Body oder kein Desktop-Ergebnis, verbindet sich der Desktop trotzdem, und die Schranke bei der Registrierung bleibt die durchsetzende Prüfung. Eine negative Antwort beendet die Verbindung mit einem Kompatibilitätsfehler.
Beide Agenten nutzen dieselben RPC-Methoden
Codex- und Claude-Sessions verwenden dieselben Methoden. Kein Methodenname enthält einen Agenten, außer den Konto- und Nutzungsmethoden im Namespace system. Der Desktop liest den Agenten aus dem Session-Datensatz und leitet jeden run.chat-Aufruf an diesen Agenten.
| Wo der Agent im Protokoll sichtbar wird | Vertrag |
|---|---|
| Session anlegen | session. |
| Laden und abstimmen | run. |
| Senden, freigeben und stoppen | run. |
| Codex-Konten | system. |
| Claude-Konten | system. |
| Nutzungsverlauf | system. |
Beide activate-Methoden sind Mutationen. Der Desktop wechselt das Konto, sendet ein Desktop-Ereignis, das den Agenten nennt, und antwortet mit der neuen Liste. Ein fehlgeschlagenes Ereignis verzögert nur das Desktop-Fenster bis zu dessen nächstem periodischen Lesen.
Die Anfrage- und Antwortstrukturen untersuchen
- Vom Telefon geschriebenrpc.request
Alles, was ein Telefon senden darf. traceparent ist Pflicht, tracestate ist optional. Der Ziel-Desktop stammt aus der Registrierung und nie aus einer Anfrage.
- Vom Relay geschriebenclientId · userId
Aus dem authentifizierten Socket übernommen. clientId ist die Route mobileDeviceId::desktopDeviceId, die der Desktop in Idempotenzschlüsseln als Gerät verwendet und die das Telefon nie sieht.
- Vom Desktop geschriebenrpc.response
Der Desktop übernimmt die id des Telefons als correlationId und antwortet damit, sodass eine einzige id den ganzen Hin- und Rückweg kennzeichnet.
- Ausstehender Eintrag(user, desktop, id)
Wird für die Desktop-Verbindung angelegt, an die die Anfrage ging. Eine Antwort wird nur zugestellt, wenn ihre id und clientId zu einem Eintrag derselben Verbindung passen, und eine finale Antwort entfernt den Eintrag.
{
"type": "rpc.request",
"payload": {
"id": "request-101",
"method": "system.ping",
"params": {},
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
}{
"type": "rpc.response",
"payload": {
"clientId": "<source-client-id>",
"id": "request-101",
"result": {
"ok": true,
"service": "desktop-relay",
"timestampMs": 1789214400000,
"capabilities": ["run.cancel.exact-run.v1"]
},
"isFinal": true
}
}Der Desktop vertraut dem Envelope nicht allein. Er vergleicht die userId des Relays mit dem Benutzer seiner eigenen Anmeldung und antwortet bei einer Abweichung mit forbidden. Er verwendet die clientId des Relays als Geräte-ID in den Idempotenz-Scope-Schlüsseln. Die Berechtigungsliste einer Relay-Anfrage ist immer rpc.
Bevor eine Antwort den Desktop verlässt, durchsucht der Sender das Ergebnis nach einem snake_case-Schlüssel. Ein einziger solcher Schlüssel ersetzt die ganze Antwort durch einen Validierungsfehler, sodass ein Handler keinen internen Feldnamen an ein Telefon durchreichen kann.
Jede Station hat ihre eigene Frist
- Desktopserver_message_handler.rs
Höchstens 8 Sekunden für einen Platz, dann 45 Sekunden Dispatch, oder 95 für run.chatLoad und run.chatLoadThread, dann 5 Sekunden, um die Antwort einzureihen. Ein Codex-Timeline-Ladevorgang hat seine eigene Frist von 90 Sekunden innerhalb der 95.
- TelefonmobileChatPolicy
Beide Apps warten 60 Sekunden auf eine ältere Seite und 115 auf den ersten Ladevorgang, länger, als der Desktop braucht. Auf eine Outbox-Übermittlung wartet die iOS-App nur 20 Sekunden, die Android-App 60. Eine Wiederholung muss dieselbe Operation und denselben Schlüssel tragen.
- Relaypending_requests.rs
Hält den ausstehenden Eintrag 90 Sekunden, 120 für die zwei ersten Ladevorgänge und 31 Minuten für run.chatOutboxSubmit. Läuft der Eintrag ab, antwortet das Relay mit -32012, und eine spätere Desktop-Antwort findet keine Route.
Auf dem Desktop wartet jeder Relay-RPC auf eine von zwei Semaphoren. 8 Plätze bedienen leichte Aufrufe. 2 Plätze bedienen schwere Aufrufe, sodass ein langsamer Medien-Lesevorgang system.ping nicht aufhalten kann.
| Schwere Gruppe | Methoden |
|---|---|
| Timeline | run. |
| Dateien und Medien | files. |
| Anhänge | files. |
| Git und Web | files. |
| Desktop-Stufe | Grenze | Antwort, wenn sie endet |
|---|---|---|
| Warten auf einen Platz | 8 Sekunden | -32603 mit dem Text „Desktop is busy; retry the request shortly.“ |
| Dispatch | 45 Sekunden, oder 95 Sekunden für run. | -32603 mit dem Text „Desktop request timed out.“ |
| Antwort in die Socket-Warteschlange stellen | 5 Sekunden | Keine. Der Desktop protokolliert den Fehler, und der ausstehende Eintrag des Relays entscheidet. |
Beide -32603-Antworten sind einfache interne Fehler ohne retryable-Kennzeichen. Ein Telefon darf sie nicht als Beweis lesen, dass nichts ausgeführt wurde.
Die Dispatch-Grenze ist ein Timeout um das Future des Handlers. An der Grenze verwirft der Desktop dieses Future, und der Handler endet an seinem nächsten await-Punkt. Eine Mutation, die dort abgeschnitten wird, lässt ihren dauerhaften Claim im Zustand running zurück. Ein Handler, dessen Arbeit zu Ende laufen muss, startet sie als eigenen Task und wartet nur auf das Ergebnis. run.chatOutboxStopAndSendNow macht das: Die Methode stoppt den Run und sendet dann die Nachricht, auch wenn das Relay nicht mehr wartet.
Eine konkrete Mutation: Eine Outbox-Nachricht übermitteln
{
"type": "rpc.request",
"payload": {
"id": "request-102",
"method": "run.chatOutboxSubmit",
"idempotencyKey": "submit-operation-201",
"params": {
"queueId": "queue-201",
"operationId": "operation-201",
"sessionId": "<existing-session-id>",
"userText": "Explain the settings flow.",
"promptText": "Explain the settings flow.",
"attachments": [],
"speedMode": "standard",
"createdAt": 1789214400000,
"updatedAt": 1789214400000,
"intent": "enqueue",
"sendMode": "queue",
"source": "composer"
}
}
}Der Remote-Adapter dekodiert WorkspaceChatOutboxSubmitRequest, prüft, dass die Session existiert, und ruft dann submit_outbox_request auf. Das Ergebnis enthält entries und receipt, und receipt hat queueId und operationId. Dieses enqueue-Beispiel speichert die Nachricht am Ende der Warteschlange und sendet sie, wenn die Session untätig ist.
Vier weitere Felder sind optional: threadId, expectedChatSettings, accessMode und expectedActiveRunId. Die Struktur weist jedes Feld zurück, das sie nicht kennt.
Um dieselbe unsichere Operation erneut zu versuchen, behalten Sie deren unveränderliche Operations-Payload und Idempotenz-Identität bei. Generieren Sie keine neue operationId, weil bei einer Netzwerkanfrage ein Timeout aufgetreten ist. Eine bearbeitete Nachricht ist eine andere Operation und muss über die explizite Queue- oder Composer-Aktion abgewickelt werden.
run.chatOutboxStopAndSendNow ist die zweite Outbox-Mutation mit einer Vorbedingung. Ihre Parameter sind sessionId, queueId, operationId, expectedRunId, expectedThreadId und expectedTurnId. Die drei expected-Werte benennen den Run, den das Telefon gesehen hat, sodass der Desktop nie einen anderen Run stoppt.
Routing ist an aktive Verbindungsgenerationen gebunden
Die Zulassung ausstehender RPCs erwirbt das Benutzer-Autoritäts-Gate und die Sperre für Routing-Übergänge. Sie weist einen Server im Draining-Zustand, eine veraltete Quellregistrierung oder eine geänderte Desktop-Verbindungsgeneration ab. Der Pending-Schlüssel enthält die Benutzer-UUID, die normalisierte Desktop-Geräte-ID und die getrimmte Anfrage-ID. Der Datensatz behält außerdem die Desktop-Verbindungs-ID und den Quell-Client.
Dies verhindert, dass eine verspätete Antwort eines vorherigen Desktop-Sockets als Ergebnis der aktuellen Anfrage behandelt wird. Ausstehende Anfragen sind auf Prozess-, Benutzer-, Desktop- und Client-Ebene beschränkt. Ein identischer vorhandener ausstehender Eintrag ist eine duplizierte Anfrage. Ein abweichender Eintrag unter demselben Schlüssel ist ein Konflikt. Keiner der beiden Fälle ist eine neue, nicht nachverfolgte Weiterleitung.
| Relay-Entscheidung | Code und Verhalten |
|---|---|
| Anfrage über dem Ratenlimit der Verbindung | -32028, retryable. Das Relay verwirft den Frame vor der Validierung und leitet nichts weiter, eine Wiederholung kann eine Mutation also nicht zweimal ausführen. Es beantwortet nur eine rpc. |
| Methode fehlt in der einkompilierten Erlaubnisliste | -32601 method-not-found. |
| API-Schlüssel-Verbindung ohne Scope | -32027 mit required |
| Mutation ohne Schlüssel | idempotency |
| Fehlerhafter Trace-Kontext oder fehlerhafte Parameter | Ein fehlerhafter Trace-Kontext und fehlerhafte session. |
| Grenze ausstehender Anfragen erreicht | -32020 mit scope, limit und current, retryable. Die Grenzen sind 4.096 pro Prozess, 512 pro Benutzer, 128 pro Desktop, 64 pro Client. |
| Dieselbe Anfrage-ID bereits ausstehend | -32021 für ein identisches Duplikat, -32022 für eine andere Anfrage unter demselben Schlüssel. IDs sind pro Benutzer und Desktop eindeutig, sodass zwei Telefone, die eine ID gegen einen Desktop wiederverwenden, kollidieren. |
| Routing hat sich unter der Anfrage verschoben | -32023, wenn sich die Desktop-Generation vor der Weiterleitung geändert hat, -32024, während die Routing-Autorität wechselt. Beide sind retryable. |
| Desktop nicht erreichbar | -32010 offline, -32011 reconnecting, -32012 Relay-Timeout, alle retryable. Ein ausstehender Eintrag läuft nach 90 Sekunden ab, nach 120 Sekunden für run. |
Zwei Schichten schützen unterschiedliche Operationen
- Anspruch im Arbeitsspeicherregistry.rs
Prozesslokal und nach einem Neustart leer. Eine Wiederholung schließt sich der laufenden Kopie an, mit bis zu 32 Wartenden pro Schlüssel. Gehalten werden höchstens 512 abgeschlossene Ergebnisse und 64 laufende Ansprüche.
- Dauerhafter Anspruchremote_rpc_idempotency
Eine SQLite-Zeile pro Benutzer, Gerät, Methode und Schlüssel, mit einem SHA-256 der Parameter. Sie übersteht Neustarts, die Tabelle hält höchstens 4.096 nicht abgelaufene Zeilen, und die Zeile wird 48 Stunden nach dem Speichern ihrer Antwort gelöscht.
- Laufende Leaselease_expires_at
Fünf Minuten ab dem Anspruch und nie verlängert. Eine noch laufende Zeile ohne lebenden Besitzer antwortet bis dahin mit busy und speichert danach eine mehrdeutige Antwort, die Wiederholungen bis zu ihrem Ablauf erhalten.
Der Scope-Schlüssel verbindet Benutzer, Gerät, Methode und Schlüssel, sodass derselbe Schlüssel von einem anderen Telefon oder für eine andere Methode ein anderer Anspruch ist, und der Fingerabdruck ist ein SHA-256 der serialisierten Parameter. Eine retryable-Zeile mit demselben Fingerabdruck wird zurückgefordert und erneut ausgeführt. Eine gespeicherte Antwort über 256 KiB wird als mehrdeutig festgehalten. 48 Stunden nach dem Speichern seiner Antwort ist ein Schlüssel vergessen, und eine Wiederholung läuft als neue Mutation.
Fehler werden nach Code klassifiziert. Ungültige Anfrage, Methode nicht gefunden, ungültige Parameter, nicht autorisiert, verboten, nicht gefunden, Konflikt, Validierung, Abrechnung, Zahlung erforderlich und nicht implementiert sind endgültig und werden wie gespeichert wiedergegeben. Parse-Fehler und alles mit retryable-Kennzeichen sind wiederholbar. Interne, Datenbank-, Fremddienst- und unbekannte Codes sind mehrdeutig. Scheitert das Erwerben des dauerhaften Claims, wird keine Mutation ausgeführt. Endet die Ausführung, ohne dass ihr Ergebnis gespeichert werden kann, meldet der Router ein mehrdeutiges Ergebnis statt eines Erfolgs, der einen fehlenden Replay-Eintrag verbirgt.
Vier Methoden dürfen nach einer unterbrochenen ersten Kopie erneut laufen: run.chatOutboxSubmit, files.appendChatAttachmentUploadChunk, files.finishChatAttachmentUpload und files.cancelChatAttachmentUpload. Jede hat ihren eigenen Domänen-Eintrag, der einen zweiten Lauf unschädlich macht. Chunk-Append gibt außerdem seinen abgeschlossenen Claim frei, weil das Upload-Ledger den Abschluss abgleicht. Der separate Fingerprint (sessionId, operationId) der Outbox schützt die Nachrichtenzustellung, nachdem der RPC-Handler zurückkehrt. Diese Schichten machen nicht jeden beliebigen Nebeneffekt zu genau einmal (exactly-once).