Skip to content

V1+: per-feature capability gating (MIN_PROTOCOL_VERSION_X) #5

Description

@yyq1025

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:

  1. 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.

  2. 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.

  3. 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.tsPROTOCOL_VERSION (auto-read from package.json) + isProtocolCompatible (uses semver caret) + helloCommand schema
  • packages/daemon/src/webrtc-peer.tsPeerSlot 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions