Refused
The host answered, and said no — an AdmitMessage.Reject carrying its raw message and structured code (#2364).
The joiner-side counterpart of the host's HostVerdict, and the same (message, code) shape us.tractat.kuilt.session.FailureReason.Refused already uses for a refusal that ended the session. Before it existed, every reject completed as WindowClosed, so "the grace window elapsed", "that token names a room I don't serve" and "I haven't noticed your drop yet" were one value — and the last of those is transient, the one case where re-joining fresh is the wrong move.
Branch on code, not message: the text is for a human reading a log. Treat an unrecognised code as retryable — RejectCode.retryable already defaults that way, and a host that predates typed codes surfaces RejectCode.Unknown.
code is deliberately required. A default would let a fake answer Refused("nope"), whose RejectCode.Unspecified is retryable, and quietly satisfy a test asking whether a terminal refusal stops the retry loop.