Relay-Wire-Protokoll
Die WebSocket-Envelopes, der Methodenvertrag, die Routing-Autorität und die Mutations-Replay-Regeln, die vom nativen Companion verwendet werden.
Gegen den Quellcode geprüft am 17. September 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 22 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.
| Namespace | Beispielmethoden |
|---|---|
| system | system.ping, system.codexAuthProfiles, system.getVoiceVocabulary |
| session | session.listProjects, session.create, session.getHistoryState |
| run | run.codexChatLoad, run.codexChatLoadPageBefore, run.codexChatOutboxSubmit, run.cancel |
| files | files.readContent, files.readBinary, files.getGitFileDiffPatch, files.beginChatAttachmentUpload |
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, und die akzeptierte Protokolluntergrenze ist das Höhere aus 1.6 und der Kompatibilitätsrichtlinie der Datenbank. Anfrage-Payloads weisen unbekannte Felder zurück, sodass ein Client, der eine userId in seine eigene Payload schreibt, den ganzen Frame scheitern lässt.
pnpm check:rpc-contractDie 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": {},
"isFinal": true
}
}Auf dem Desktop wartet jeder Relay-RPC auf eine von zwei Semaphoren, 8 gleichzeitige leichte Aufrufe und 2 gleichzeitige schwere wie Timeline-Ladevorgänge, Abstimmung, Medien, Anhänge und Git-Diffs. Mehr als 8 Sekunden Warten liefert busy, und der Dispatch ist auf 45 Sekunden begrenzt, oder 95 für run.codexChatLoad und seine Thread-Variante.
Eine konkrete Mutation: Eine Outbox-Nachricht übermitteln
{
"type": "rpc.request",
"payload": {
"id": "request-102",
"method": "run.codexChatOutboxSubmit",
"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; receipt hat queueId und operationId. Dieses enqueue-Beispiel speichert die Nachricht am Ende der Warteschlange und sendet sie automatisch, wenn die Session untätig ist.
Um dieselbe unsichere Operation erneut zu versuchen, behalten Sie deren unveränderliche Operations-Payload und Idempotenz-Identität bei. Generieren Sie keine neue operationId nur deshalb, 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.
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 Pending-Datensatz ist eine duplizierte Anfrage; ein abweichender Datensatz unter demselben Schlüssel ist ein Konflikt. Keiner der beiden Fälle ist eine neue, nicht nachverfolgte Weiterleitung.
| Relay-Entscheidung | Code und Verhalten |
|---|---|
| Methode fehlt in der einkompilierten Erlaubnisliste | -32601 method-not-found. |
| API-Schlüssel-Verbindung ohne Scope | -32027 mit requiredScopes, retryable false. Ein PlanToCode-JWT trägt immer rpc, read und write, ein Telefon sieht dies also nie. |
| Mutation weist keinen idempotencyKey auf | -32026, Grund protocolUpdateRequired, retryable false. |
| Fehlerhafter Trace-Kontext oder fehlerhafte session.syncHistoryState-Parameter | Vor der Weiterleitung abgewiesen. Das ist die einzige Methode, deren Parameter das Relay typprüft; jede andere Payload ist für das Relay undurchsichtig und wird auf dem Desktop validiert. |
| 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 retryable. |
| Desktop nicht erreichbar | -32010 offline, -32011 reconnecting, -32012 Relay-Timeout, alle retryable. Ein Eintrag läuft nach 90 Sekunden ab, oder 31 Minuten für run.codexChatOutboxSubmit, und eine Desktop-Trennung beantwortet jede ausstehende Anfrage dieser Generation sofort mit -32011. |
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, und 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.
Ein Replay nach einer Unterbrechung ist methodenspezifisch. Chunk-Append kann den Abschluss über sein eigenes Domänen-Ledger abgleichen. 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).