Context
V0 has a single wire-protocol-version concept owned by @sidecodeapp/protocol:
PROTOCOL_VERSION read directly from packages/protocol/package.json at module load (single source — bumping = npm version patch on the protocol package).
isProtocolCompatible(remote: string): boolean exposes npm caret-range semantics via the semver package — proper handling of pre-release tags, build metadata, the 0.0.x exact-match rule, and the ≥1.x same-major rule.
- Daemon and iOS both import + report the same value; hello/server_info each carry one
protocolVersion: string field.
- Mismatch → daemon sends
error{code:"incompatible_protocol"} + closes the DataChannel.
This is sufficient for solo dev — every wire-schema change forces both ends to update together.
What's missing (do before public launch)
Once we ship publicly and want to support users on an older app talking to a newer daemon at per-feature granularity, the "either fully compatible or not at all" check isn't enough. Some additive features can be silently omitted for older clients (forward-compat via Zod passthrough) without forcing a wire-version bump and a forced reconnect.
That's capability gating. We'll need:
-
MIN_PROTOCOL_VERSION_X constants per feature in a new packages/daemon/src/capabilities.ts:
const MIN_PROTOCOL_VERSION_FLEXIBLE_EDITOR_IDS = "1.5.0";
const MIN_PROTOCOL_VERSION_VOICE_INPUT = "1.8.0";
Expressed in PROTOCOL_VERSION terms, not iOS app release version — the protocol package's version already represents "what wire features does this client understand" because client ships with a specific protocol package version baked in.
-
Daemon-side gating before sending a frame field that requires capability X:
if (semver.gte(slot.protocolVersion, MIN_PROTOCOL_VERSION_X)) {
frame.x = value;
}
Slot already has protocolVersion from the hello frame; just expose it through CommandContext.
-
Document the version policy in CONTRIBUTING — when each kind of change is appropriate:
- patch bump: additive optional field that older clients ignore (no gating needed beyond Zod passthrough)
- patch bump +
MIN_PROTOCOL_VERSION_X: additive REQUIRED field that older clients can't render but daemon can omit (e.g. new tool output detail variant — daemon-side gates per call)
- minor bump (during 0.x) / major bump (post-1.0): breaking schema — forces incompatibility, no gating possible
Why this is deferred
- V0 is solo-dev — one user (me), daemon + app updated together, zero coexistence pressure.
- We have zero V1 features that older clients can't render. The infrastructure would be unused.
semver package is already installed (used by isProtocolCompatible), so item 1 doesn't carry a new dep cost — it's purely "wire some constants and a per-feature check."
Trigger
Reopen / re-prioritize when any of:
- First public release planned (
.dmg published, App Store TestFlight invited beyond solo)
- Second feature added that older apps can't render but daemon could silently omit
- Multi-user testing reveals "your app is too old to use feature X" UX gap
Items dropped from earlier issue
The earlier version of this issue called for re-introducing an appVersion field separate from protocolVersion. That was wrong — PROTOCOL_VERSION already IS the relevant signal for capability gating (the version of the protocol package the client shipped with). No separate iOS-release-version concept needed; capabilities map cleanly to PROTOCOL_VERSION ranges.
The earlier version also called for adding the semver package. That landed alongside the version-handshake unification — already done.
Related code
packages/protocol/src/index.ts — PROTOCOL_VERSION (auto-read from package.json) + isProtocolCompatible (uses semver caret) + helloCommand schema
packages/daemon/src/webrtc-peer.ts — PeerSlot would need a protocolVersion field stashed from hello; CommandContext would need a getter; capability gating would happen in frame builders downstream
Reference
Paseo's MIN_VERSION_* pattern in their websocket-server.ts — single-source-of-truth file with constants, gating threaded through frame builders. Same shape, just using PROTOCOL_VERSION instead of app release version as the comparison axis.
Context
V0 has a single wire-protocol-version concept owned by
@sidecodeapp/protocol:PROTOCOL_VERSIONread directly frompackages/protocol/package.jsonat module load (single source — bumping =npm version patchon the protocol package).isProtocolCompatible(remote: string): booleanexposes npm caret-range semantics via thesemverpackage — proper handling of pre-release tags, build metadata, the 0.0.x exact-match rule, and the ≥1.x same-major rule.protocolVersion: stringfield.error{code:"incompatible_protocol"}+ closes the DataChannel.This is sufficient for solo dev — every wire-schema change forces both ends to update together.
What's missing (do before public launch)
Once we ship publicly and want to support users on an older app talking to a newer daemon at per-feature granularity, the "either fully compatible or not at all" check isn't enough. Some additive features can be silently omitted for older clients (forward-compat via Zod passthrough) without forcing a wire-version bump and a forced reconnect.
That's capability gating. We'll need:
MIN_PROTOCOL_VERSION_Xconstants per feature in a newpackages/daemon/src/capabilities.ts:Expressed in PROTOCOL_VERSION terms, not iOS app release version — the protocol package's version already represents "what wire features does this client understand" because client ships with a specific protocol package version baked in.
Daemon-side gating before sending a frame field that requires capability X:
Slot already has
protocolVersionfrom the hello frame; just expose it throughCommandContext.Document the version policy in CONTRIBUTING — when each kind of change is appropriate:
MIN_PROTOCOL_VERSION_X: additive REQUIRED field that older clients can't render but daemon can omit (e.g. new tool output detail variant — daemon-side gates per call)Why this is deferred
semverpackage is already installed (used byisProtocolCompatible), so item 1 doesn't carry a new dep cost — it's purely "wire some constants and a per-feature check."Trigger
Reopen / re-prioritize when any of:
.dmgpublished, App Store TestFlight invited beyond solo)Items dropped from earlier issueThe earlier version of this issue called for re-introducing an
appVersionfield separate fromprotocolVersion. That was wrong — PROTOCOL_VERSION already IS the relevant signal for capability gating (the version of the protocol package the client shipped with). No separate iOS-release-version concept needed; capabilities map cleanly to PROTOCOL_VERSION ranges.The earlier version also called for adding the
semverpackage. That landed alongside the version-handshake unification — already done.Related code
packages/protocol/src/index.ts—PROTOCOL_VERSION(auto-read from package.json) +isProtocolCompatible(uses semver caret) +helloCommandschemapackages/daemon/src/webrtc-peer.ts—PeerSlotwould need aprotocolVersionfield stashed from hello;CommandContextwould need a getter; capability gating would happen in frame builders downstreamReference
Paseo's
MIN_VERSION_*pattern in theirwebsocket-server.ts— single-source-of-truth file with constants, gating threaded through frame builders. Same shape, just using PROTOCOL_VERSION instead of app release version as the comparison axis.