ci / rust-arm64 (pull_request) Failing after 31s
android / android (pull_request) Failing after 1m9s
ci / docs-site (pull_request) Successful in 1m23s
apple / swift (pull_request) Successful in 2m4s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 2m23s
ci / web (pull_request) Successful in 5m45s
ci / rust (pull_request) Failing after 10m40s
nix / flake (pull_request) Successful in 13m46s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 1m47s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 1m47s
ABI 19 -> 20. Wire protocol unchanged (still 2). Persisting the mgmt port (fe2bfeca) made a moved port survive mDNS going away, but mDNS was still the only SOURCE: a host that had never been seen on it — VPN-only, a routed subnet, or simply added by address on a network where multicast has never worked — had nothing to learn from and fell back to 47990. The `Welcome` now carries the port, so the client learns it over the connection it has already authenticated and discovery stops being involved at all. `Welcome.mgmt_port`, a trailing u16 after the cipher block, following the same additive discipline as the eight fields before it (compositor, gamepad, bitrate_kbps, bit_depth, color, chroma_format, audio_channels, codec): an older peer stops earlier and gets a documented default, in both directions, so WIRE_VERSION does not move. ⚠ THE TRAP, and why emitting the port forces the `cipher` placeholder: `cipher` is emitted only when non-default, so appending the port to an AES Welcome would land its LOW BYTE at offset 68 — exactly where every shipped 0.28.x client reads `cipher`, whose decode is deliberately fail-closed on an unknown id. 47991 is 0xBB57, so byte 68 would read 0x57 = 87, and EVERY current client would fail the handshake against a host that had merely moved its mgmt port. `encode` therefore writes an explicit cipher byte whenever a port rides along (the placeholder discipline `Hello::encode` already uses); a current client reads AES, a pre-cipher client stops before 68 regardless. The test pins the byte, both offsets (69 AES / 101 ChaCha), and that a host advertising no port still emits exactly 68 bytes — this field costs the common case nothing. Host: `mgmt::effective_port()` reads the same resolved bind `publish_endpoint` writes, so the wire, the endpoint file and the mDNS TXT cannot disagree — one lookup, not a fourth place to compute a port. `0` on the standalone punktfunk1-host binary, which has no management API: advertising 47990 from a host that is not serving it would be worse than saying nothing. Clients persist it on connect, feeding the store plumbing fe2bfeca already built: * Rust — `on_connected` grew the port alongside the fingerprint, plus `learn_mgmt_port_by_fp` (keyed by fingerprint alone, the identity a just-connected client is certain of). * Apple — `PunktfunkConnection.hostMgmtPort` + `updateMgmtPort` at the existing markConnected site. * Android — a new `nativeHostMgmtPort` JNI call, persisted where the session is constructed. Verified: Linux (pf-lxcheck2, amd64) `cargo check --all-targets` clean across punktfunk-core, pf-host-config, punktfunk-host, pf-client-core, pf-presenter, punktfunk-cli, punktfunk-client-linux and punktfunk-client-session, each confirmed genuinely compiled (counting `Compiling` as well as `Checking` — cargo prints the former for bin-only packages, which is what made an earlier gate look vacuous when it was not). punktfunk-core quic tests 76/76. Android: :kit+:app Kotlin, ParseRecordTest 12/12, and cargoNdkClippy clean for aarch64-linux-android. Apple: xcframework rebuilt at ABI 20, `swift build` complete. cargo fmt --all --check clean. NOT verified: the Windows client (192.168.1.133 unreachable).