-
Notifications
You must be signed in to change notification settings - Fork 0
Mesh Protocol
The watchdog talks to proxy.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.
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 v2 -- resolve request -> resolved / fallback_native response. v3 only changes the handshake.
%LOCALAPPDATA%Low\WKVRCProxy\v3_welcome_cache.json -- same LocalLow state-root as clean_exit.flag, codec-state.json, etc. (see Logs and Diagnostics).
Per-host keying: current clients key the welcome cache on proxy.whyknot.dev; legacy clients and fallback discovery may still use node1.whyknot.dev or node2.whyknot.dev. Cache entries remain keyed on the hostname so mixed deployments do not cross-serve welcome state.
Atomic write via <file>.new -> File.Move(overwrite:true). A crash mid-write leaves either the old or new file intact, never half-written.
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.
A subset of frames is always JSON Text regardless of the negotiated format: pong, ping, protocol_error, rate_limited. A multi-hour reconnect storm in production traced to a server-side regression that routed pongs through the binary path on msgpack-negotiated connections; the watchdog's hot-path msgpack decoder didn't recognise them and default-discarded, the connection eventually idled out, the client reconnected, repeat. The current client recognises those four control actions on the binary dispatch path as defense-in-depth even though the server's invariant is to always send them as JSON-Text. If you add a new control-style frame (server -> client, low-frequency, not part of the resolve hot path), the rule is: send as JSON-Text, recognise on both Text and Binary paths client-side.
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
ShouldSendClientHelloreturns false based on the echoed subprotocol. - No
client_hellois 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=falselog line inwatchdog-<utc>.logso the diagnostic is available without any console churn.
- Doesn't change the resolve hot path.
resolverequest andresolved/fallback_nativeresponse 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.
- Engineering Standards -- wire-protocol contributor rules.
-
src/WKVRCProxy.Shared/Protocol.cs-- DTO definitions, byte-exact with whyknot.dev'sMeshResolveProtocol.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.
Start here
For users
Reference
For maintainers