Skip to content

Mesh Protocol

github-actions[bot] edited this page May 5, 2026 · 4 revisions

Mesh Protocol

The watchdog talks to whyknot.dev over a single persistent WebSocket. The wire shape has versioned forward/backward like an HTTP API does — the watchdog can talk to any server back to v1 (legacy) and any server up to its own client version.

What v3 added (current — 2026-05-04)

Subprotocol: whyknot-v3 literal in the WebSocket upgrade. If the server (or an intermediate proxy) doesn't echo it back, the client silently falls back to v2 behavior. The user-visible console doesn't change either way.

Compression: RFC 7692 permessage-deflate offered on the upgrade. Server can refuse — the connection is uncompressed-but-functional in that case. When negotiated, every frame is compressed before it leaves the server's egress, so the welcome and resolve_log streams are roughly half their v2 size on the wire.

Welcome cache: The server's welcome frame is mostly unchanging across reconnects (engine list, feature list, version strings). v3 adds a welcome_hash field — an opaque server-side fingerprint. The client persists it per-node and offers it back on the next reconnect via a small client_hello frame:

[upgrade] Sec-WebSocket-Protocol: whyknot-v3

client → server (FIRST FRAME):
  {"action":"client_hello","welcome_hash":"<cached-hash or null>","client_id":"<process-guid>"}

server → client:
  EITHER (cache hit, small):
    {"action":"welcome_cached","protocol_version":3,"node":"node1","warp_active":true}
  OR (cache miss, full welcome with new hash to cache):
    {"action":"welcome", "protocol_version":3, "node":..., "engines":[...],
     "features":[..., "v3_compression", "welcome_hash_ack"], "yt_dlp_version":...,
     "server_version":..., "welcome_hash":"<new-fingerprint>"}

After the welcome, the resolve hot path is byte-exact identical to v2resolve request → resolved / fallback_native response. v3 only changes the handshake.

Cache file location

%LOCALAPPDATA%Low\WKVRCProxy\v3_welcome_cache.json — same LocalLow state-root as clean_exit.flag, codec-state.json, etc. (see Logs and Diagnostics).

Per-node keying: apex-302 routes between node1 and node2, and their welcome contents differ (different yt-dlp versions, different node labels, different warp_active state). Cache is keyed on the hostname so node-rotation doesn't thrash.

Atomic write via <file>.newFile.Move(overwrite:true). A crash mid-write leaves either the old or new file intact, never half-written.

What v3.1 added (current)

Asymmetric format negotiation. Client → server stays JSON over WebSocket Text frames; server → client post-welcome picks JSON-Text or MessagePack-Binary based on what the client offered. Control frames (welcome / welcome_cached / client_hello) stay JSON either way for debuggability + first-byte simplicity.

The choice rides on the existing client_hello frame:

client → server (FIRST FRAME, JSON-Text):
  {
    "action": "client_hello",
    "welcome_hash": "<cached or null>",
    "client_id": "<persistent guid>",
    "accept_formats": ["msgpack", "json"]   ← v3.1 NEW (preference-ordered)
  }

server → client (welcome, JSON-Text):
  {
    "action": "welcome", ...,
    "negotiated_format": "msgpack"          ← v3.1 NEW. "json" or "msgpack".
  }

Server picks the first format from accept_formats it supports. Older servers (v2 / v3.0) ignore the unknown field and the response defaults to json — universal fallback baseline. Choice is fixed for the connection's lifetime; no per-frame negotiation.

After the welcome, the hot path (resolved / fallback_native / resolve_log) arrives as MessagePack Binary frames when the server picked msgpack. Wire savings measured live: 60-72% smaller than JSON, ~67% faster to decode. Wrapper still receives JSON over the local pipe — the watchdog transcodes msgpack → JSON before the pipe write so the wrapper stays simple and small.

Backward compat (v2 fallback)

When the server doesn't accept whyknot-v3:

  • v2 servers don't recognize the subprotocol → echo it back as null/empty.
  • Cloudflare in some configs strips unrecognized WS subprotocol headers → server never sees it.
  • Older server builds before v3.0 deployed.

In all three cases:

  • Client's ShouldSendClientHello returns false based on the echoed subprotocol.
  • No client_hello is sent.
  • Existing v2 path runs unchanged: server emits its plain welcome, client deserializes it, hot path operates as it has since v2.
  • File-only [mesh][v3] negotiated subprotocol=<none> v3=false log line in watchdog-<utc>.log so the diagnostic is available without any console churn.

What v3 does NOT do

  • Doesn't change the resolve hot path. resolve request and resolved / fallback_native response shapes are identical to v2.
  • Doesn't introduce a local HTTP proxy server (the parked Part E adapter design — separate, larger work).
  • Doesn't change the wrapper (Tools/yt-dlp.exe). v3 is a watchdog ↔ mesh-server concern only.

See also

  • Engineering Standards — wire-protocol contributor rules.
  • src/WKVRCProxy.Shared/Protocol.cs — DTO definitions, byte-exact with whyknot.dev's MeshResolveProtocol.cs.
  • src/WKVRCProxy/MeshClient.cs — connection lifecycle + dispatch.
  • src/WKVRCProxy/WelcomeCache.cs — cache persistence.
  • src/WKVRCProxy/MeshJsonContext.cs — source-gen JSON metadata for the v3 DTOs.

Clone this wiki locally