Zum Artikel springen
PlanToCodeDocsApp herunterladen

HandbuchRelay & Konten

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

Jede Station prüft Anfragen gegen ihre eigene Kopie der Methodenliste
Jede Station prüft Anfragen gegen ihre eigene Kopie der MethodenlisteKein laufendes Programm liest rpc-contract.v1.json. Die Telefon-App, das Relay und der Desktop-Router führen jeweils eine eigene Methodenliste und lehnen ab, was darin fehlt, in dieser Reihenfolge. Vor jeder Auslieferung liest pnpm check:rpc-contract das Manifest und jede Kopie und schlägt bei jeder Abweichung fehl. Bei den Telefon-Apps vergleicht es nur die Methodennamen.
vor der Auslieferung
rpc-contract.v1.json65 Methoden · 22 Mutationen
Vertragsprüfungpnpm check:rpc-contract
liest
liest jede Kopie
bei jeder Anfrage
Telefon-AppRpcMethod-Enum
Relayrequest_validation.rs
Desktop-Routerrouter/mod.rs · Handler
nur Namen
eine Methode außerhalb des Enums oder eine Mutation ohne Schlüssel wird nie gesendet
-32601Methode fehlt in seiner Liste-32027API-Schlüssel ohne nötigen Scope-32026Mutation ohne Schlüssel
-32601unbekannte Methode-32602leerer oder zu langer Schlüssel-32602unbekanntes oder snake_case-Feld
  • 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.

NamespaceBeispielmethoden
systemsystem.ping, system.codexAuthProfiles, system.getVoiceVocabulary
sessionsession.listProjects, session.create, session.getHistoryState
runrun.codexChatLoad, run.codexChatLoadPageBefore, run.codexChatOutboxSubmit, run.cancel
filesfiles.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.

Vergleichen Sie jede Kopie vom Repository-Root aus mit dem Manifest
pnpm check:rpc-contract

Die Anfrage- und Antwortstrukturen untersuchen

Das Relay ergänzt, wer fragt und wohin die Antwort geht
Das Relay ergänzt, wer fragt und wohin die Antwort gehtEin Telefon sendet nur, worum es bittet. Das Relay ergänzt aus dem authentifizierten Socket, wer fragt, und legt einen ausstehenden Eintrag für die Desktop-Verbindung an, an die es weiterleitet. Der Desktop antwortet mit derselben id und Route, die Antwort wird nur zugestellt, wenn sie zu diesem Eintrag passt, und das Relay entfernt die Route, bevor das Telefon die Antwort sieht.
Telefon
Relay
Desktop
Identität stammt aus diesem Socket
rpc.request
idmethodparamsidempotencyKeytraceparent
rpc.request
clientIduserIdtraceparentidmethodparamsidempotencyKey
angelegt
Ausstehender Eintrag(user, desktop, id) → Route, Desktop-Verbindung
jedes andere Feld, etwa userId oder ein Ziel-Desktop, lässt den ganzen Frame scheitern
eine userId, die nicht zum angemeldeten Konto gehört, ist verboten
rpc.response
clientIdidresulterrorisFinaltraceparent
rpc.response
idresulterrorisFinaltraceparentclientId
muss passen
  • 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.

Companion → Relay: eine lesende RPC-Anfrage
{
  "type": "rpc.request",
  "payload": {
    "id": "request-101",
    "method": "system.ping",
    "params": {},
    "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
  }
}
Desktop → Relay: Struktur des Antwort-Envelopes; result-Inhalt ist methodenspezifisch
{
  "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

Übermittlungsstruktur – ersetzen Sie Identitäten durch eine reale Session; Zeitstempel dienen der Veranschaulichung
{
  "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-EntscheidungCode 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-ParameterVor 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

Was eine wiederholte Mutation erhält, hängt davon ab, wann sie ankommt
Was eine wiederholte Mutation erhält, hängt davon ab, wann sie ankommtDie erste Kopie beansprucht ihren Schlüssel im Arbeitsspeicher und in SQLite. Eine Wiederholung, die während der Ausführung ankommt, wartet auf deren Antwort, eine spätere erhält die gespeicherte Antwort, und 48 Stunden nach dem Speichern der Antwort ist der Schlüssel vergessen und eine Wiederholung läuft erneut. Wird die erste Kopie abgebrochen, bleibt ihre SQLite-Zeile running, sodass Wiederholungen busy erhalten, bis die 5-Minuten-Lease endet, und danach eine mehrdeutige Antwort.
Erste Kopiewird fertig
Erste Kopiewird abgebrochen
erste Kopie läuft
Antwort aufbewahrt
eine Wiederholung kommt an
wartet auf dieselbe Antwort
gespeicherte Antwort, nichts läuft
Schlüssel vergessen, läuft erneut
läuft
Wiederverbindung oder Neustart
Zeile läuft noch
busy, nichts läuft
mehrdeutig, läuft nie
Anspruch
Antwort liegt vor
Anspruch + 5 min
gespeichert + 48 h
nicht maßstäblich
replay-sichere Methoden laufen stattdessen erneut
gleicher Schlüssel, andere Parameter → Konflikt · neuer Schlüssel bei 4.096 Zeilen → Kapazitätsgrenze
  • 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).