a73x

the wire cannot say WHY an attach was refused, so the client guesses in the message

open   by a73x

Five refusal causes (see the sibling issue on counting and logging them)
arrive at the client as `exit_status` with a single byte, which is also what
a shell exiting 1 sends. The client cannot recover the reason, only a
binary — it infers refusal from "no state landed since this attach":

```zig
const landed = core.rep.state_since_attach;
state = if (landed) .exited else .refused;
```

So the user-facing line has to guess out loud:

    mux: attach refused or no state received (session full?)

The guess was wrong in the field on 2026-08-23: the table was not full at
the moment of the refusal, and the parenthetical sent the search in the
wrong direction.

Sketch: a new daemon -> client frame carrying a reason byte, sent BEFORE the
existing `exit_status`. Backward compatible in both directions — a client
older than the frame drops what it does not recognise (`else => {}` in its
dispatch, the same property `PendingSwitch` in client.zig documents and
builds its deadline around), so an old client sees exactly today's
behaviour. A new client against an old daemon never gets the frame and
falls back to the `state_since_attach` inference it already has.

Wants an xversion leg both ways.

Comments

a73x   2026-08-23 12:35

Reproduced, and the message is worse than "cannot say why": it names the
WRONG table.

Rig — isolated XDG_STATE_HOME, one daemon, ONE session, clients added one at
a time and gated on the daemon's own `clients=` count after each:

    client 1..8: attached (clients=8)
    client 9: DID NOT SEAT (clients=8) -- its output:
    mux: attach refused or no state received (session full?)
    === final ===
    clients=8 attaches=8 sessions=1

`sessions=1` against `max_sessions = 4`: three session slots free, the
session the 9th asked for (`0`, the bare-`mux` default) alive and attachable.
The refusal was purely `max_clients = 8`. The parenthetical sent the reader
to the session table, which was the one thing that was fine.

That is not a hypothetical misread. Triaging the 2026-08-23 lockout I took
the hint, spent a full pass on `max_sessions` and a stray `mux --session
test`, and had to reconstruct the session table from /proc before finding
the real cap.

Second half of the trap, also measured: a wall costs one client slot PER
TILE, so the 8 are easier to exhaust than the number suggests. One bare
`mux wall` hydrating a 3-line wall file, from a baseline of clients=0:

    clients=3 attaches=7 sessions=3
    session 0 clients=1  session 1 clients=1  session 2 clients=1

Two walls and two plain clients is the whole table. In the field five `mux`
processes were enough.

So the reason byte wants to distinguish at least "client table full" from
"session table full" — they are different limits, different fixes (detach a
client vs. end a session), and today's text asserts the wrong one of the two
with no hedge on the noun.