/// Client-side tuning constants (frozen contract; SOLE value source is the
/// plan §3.2.1 table — adding/changing a constant goes back through T-iOS-3).
/// All named — no magic numbers anywhere downstream.
public enum Tunables {
    /// WS keep-alive ping period. `URLSessionWebSocketTask` has NO automatic
    /// ping (plan §1) — `PingScheduler` drives an explicit `sendPing` at this
    /// interval so the terminal is never "looks connected but dead".
    public static let pingInterval: Duration = .seconds(25)

    /// Consecutive missed pongs after which the connection is declared dead
    /// (T-iOS-5): 1 missed pong is tolerated, the 2nd is a disconnect signal.
    public static let pongMissLimit = 2

    /// Foreground `/live-sessions` polling period. Mirrors the web launcher's
    /// refresh cadence (public/launcher.ts:30 `REFRESH_MS = 5000`).
    public static let listPollInterval: Duration = .seconds(5)

    /// Telemetry chips grey out when `StatusTelemetry.at` is older than this.
    /// Mirrors public/tabs.ts:45 `STATUSLINE_TTL_MS` (= server default,
    /// src/config.ts:63). The server value is env-overridable at runtime while
    /// iOS bakes the default — accepted drift (plan §3.2.1).
    public static let telemetryStaleTtlMs: Int = 30_000

    /// Away-digest banner auto-fade delay (T-iOS-14).
    public static let digestFadeDelay: Duration = .seconds(8)

    /// Max session-title length after sanitisation (T-iOS-23; OSC titles are
    /// host/attacker-controlled input).
    public static let titleMaxLength = 256

    /// Overall pairing-probe deadline (§3.4 contract ruling, 2026-07-04): if
    /// either probe step hangs past this, the probe resolves `.timeout`.
    public static let pairingProbeTimeout: Duration = .seconds(10)

    /// `URLSessionWebSocketTask.maximumMessageSize`. The default (1 MiB) is too
    /// small: ring-buffer replay arrives as ONE full-snapshot frame
    /// (src/session/session.ts:165-171) and JSON escaping inflates control
    /// bytes to `\uXXXX` by 1-6x (src/protocol.ts:186), so worst case is
    /// ≈ 6 × SCROLLBACK_BYTES (default 2 MiB) plus frame envelope → 16 MiB.
    ///
    /// COUPLING WARNING: SCROLLBACK_BYTES is a server env knob the client
    /// cannot discover at runtime (no config handshake). If the host raises it
    /// past ~2.7 MB (or replay is extremely escape-dense), `receive()` fails
    /// with NSPOSIXErrorDomain code 40 (EMSGSIZE "Message too long"; 55
    /// ENOBUFS as fallback classification — T-iOS-2 spike measured) — that MUST
    /// be classified as the non-retryable `.replayTooLarge` failure and never
    /// fed into the backoff reconnect loop (plan §3.2 / T-iOS-9/10).
    public static let maxWSMessageBytes = 16 * 1024 * 1024
}
