This document defines how the Wired protocol evolves across versions, and the contract that client and server implementations agree to follow so that peers running different minor versions can interoperate gracefully.
It applies to both WiredSwift (this repository) and downstream clients such
as Wired-macOS, which embed the same protocol parser.
The Wired protocol version (declared in Sources/WiredSwift/Resources/wired.xml
and exchanged in the handshake under p7.handshake.protocol.version) follows
semantic versioning:
| Component | Meaning |
|---|---|
Major (X.y.z) |
Wire-format break — field IDs reused, types changed, or framing redefined. Peers with different majors refuse to connect. |
Minor (x.Y.z) |
Backward- and forward-compatible additions: new optional fields, new optional messages, new enum values. Peers with different minors must interoperate. |
Patch (x.y.Z) |
Documentation, clarifications, or implementation fixes that do not change the wire. |
The P7 framing version (p7.handshake.version) follows the same rules — peers
with the same P7 major proceed; different majors abort the handshake.
After the regular handshake exchanges versions, each peer stores the negotiated
versions on the P7Socket:
negotiatedProtocolVersion—min(local, remote)of the Wired version.negotiatedBuiltinProtocolVersion—min(local, remote)of P7.
When the two Wired versions differ (any minor difference), the existing
p7.compatibility_check.specification exchange runs in both directions: each
peer ships its wired.xml, parses what it receives, and computes a
CompatibilityDiff describing which messages and fields the peer does not
know. The exchange is never rejected — the result is informational and
drives the runtime behaviour described below.
P7Socket exposes:
remoteSpec: P7Spec?— the peer's parsed spec (when available).compatibilityDiff: CompatibilityDiff— symmetric set difference of message and field IDs.peerKnows(messageNamed:)/peerKnows(fieldNamed:)— convenience helpers.
The P7 binary frame is TLV-ish: only string, data, and list types carry
an explicit 4-byte length prefix. Fixed-size types (bool, enum, uint32,
uint64, int32, int64, double, date, uuid, oobdata) derive their
size from the spec.
This has a forward-compat consequence: a peer that does not know a field ID
cannot reliably skip a fixed-size field — its size is unknown without the
spec. The parser therefore stops decoding the rest of the message body when it
hits an unknown field, and records the offending ID in
P7Message.unknownFieldIDs for diagnostics.
Rule for protocol authors: when adding a new optional field to the
protocol, prefer a length-prefixed type (string, data, list) so that
older peers — which do not know the new field — can fall through gracefully
without aborting the rest of the message decode. If a fixed-size type is
genuinely required (e.g., uint32 or bool), the new field must be paired
with the diff machinery: senders must consult
P7Socket.peerKnows(fieldNamed:) before adding it to a message bound for the
peer, or the field will be filtered out automatically by P7Socket.write(_:)
based on compatibilityDiff.fieldsUnknownToRemote.
Every <p7:field>, <p7:message>, and <p7:enum> introduced after the
initial 3.0 baseline must carry a version="X.Y" attribute matching the
Wired protocol version in which it was added. This is what makes the
CompatibilityDiff meaningful and lets us tell at a glance which spec items
are newer than a given peer's view of the world.
Existing items are versioned historically (2.0, 2.5, 3.0, etc.). Do not
remove or backdate the version attribute on existing items — that breaks the
historical record.
- Choose the smallest unused field ID in the relevant range.
- Prefer a length-prefixed type unless a fixed size is required.
- Add
version="X.Y"matching the next Wired minor version on the entry. - If the field is fixed-size:
- On the sender side, rely on the automatic filter in
P7Socket.write(_:)(driven bycompatibilityDiff.fieldsUnknownToRemote); do not add bespokeif peerKnows(...)guards unless the absence of the field changes the semantic meaning of the message and you need to take a different code path.
- On the sender side, rely on the automatic filter in
- If the field is required (not optional), the change is major — bump the protocol major version and document the break.
- Bump
version="X.Y"on the<p7:protocol>root.
- Choose the smallest unused message ID in the relevant range.
- Add
version="X.Y"matching the next Wired minor version. - Senders should rely on the automatic message filter in
P7Socket.write(_:)for messages whose absence is benign (notifications, broadcasts). For messages that expect a response, prefer an explicitif socket.peerKnows(messageNamed: "...")guard, falling back to a compatible behaviour when the peer cannot handle the message. - Receivers automatically tolerate unknown message IDs: the frame is logged
and any recognised fields (notably
wired.transaction) are still extracted so the upper layer can decide whether to reply withwired.error.unrecognized_message.
Removing a field or message, or repurposing an ID for a different type, is a major version break. Rename the spec entry instead and keep the old one around as deprecated until the next major bump. Field/message IDs are part of the wire and must be treated as permanent within a major.
Both P7Message and P7Socket tolerate forward-compatible drift:
- Unknown message IDs are decoded best-effort (known fields extracted) and
flagged via
P7Message.hasUnknownMessageID. - Unknown field IDs abort decoding of the rest of the message body and
are recorded in
P7Message.unknownFieldIDs. The fields decoded so far are preserved. - The
compatibility_checkexchange never throws — incompatibilities are logged at WARNING level.
This combination of receiver tolerance, optional capability diff, and the length-prefix rule allows a v3.1 client to talk to a v3.0 server (and vice versa) without rebuilding either side.