a73x

b44db225

docs: MUX_SOCK/MUX_SESSION in the README, and why the loop is refused not escaped

a73x   2026-08-19 17:17

Commit message
docs: MUX_SOCK/MUX_SESSION in the README, and why the loop is refused not escaped

The decision entry records what changed the answer: while Ctrl-\ was a
single detach key an inner client could be told to leave, so a self-attach
was survivable. As a prefix it is not — both clients filter it and the
outer keyboard has no spelling that reaches past the inner one — so there
is no escape chord left to document and the loop has to be prevented.

README.md
Old New
@@ -52,6 +52,16 @@ order and wrap at the ends, and `mux --session NAME` attaches to any of
52 them by name (creating it if there is room). With only one session open 52 them by name (creating it if there is room). With only one session open
53 `n` and `p` do nothing — there is nowhere to step to. 53 `n` and `p` do nothing — there is nowhere to step to.
54 54
55 Every session shell is told where it lives: `$MUX_SOCK` is the daemon's
56 socket path and `$MUX_SESSION` is the session's name. Scripts and prompts
57 can read them, and `mux` reads them to refuse attaching to the very session
58 it is running in — that attach would feed the session's own paint back into
59 itself, take the alternate screen and swallow every keystroke, with no
60 outer keyboard able to reach it. Attaching from one session to *another* on
61 the same daemon is fine and unaffected; so is `mux wall`, unless one of its
62 tiles is the session it was launched from. Unset `MUX_SESSION` if you
63 really want the loop.
64
55 ## Quick start, remote over ssh 65 ## Quick start, remote over ssh
56 66
57 Works anywhere ssh works. On the remote host: put `muxd` on PATH, then: 67 Works anywhere ssh works. On the remote host: put `muxd` on PATH, then:
docs/decisions.md
Old New
@@ -4021,3 +4021,65 @@ by a reply that outlived its connection. And the silence already has an
4021 explanation they have been shown, since the reconnect painted its own banner. 4021 explanation they have been shown, since the reconnect painted its own banner.
4022 Reporting both would put two markers in one corner about one event. 4022 Reporting both would put two markers in one corner about one event.
4023 4023
4024 **A client refuses the session it is standing in, because it can no longer be
4025 rescued.** `mux --sock S` run from a shell inside session 0 of the daemon on S
4026 attached that session to itself. The inner client painted into the grid it was
4027 reading, so paint became a delta became a repaint; it took the alternate screen
4028 and, being the terminal's foreground reader, it swallowed every keystroke. That
4029 was survivable while `Ctrl-\` was a single detach key — one chord went to the
4030 inner client and it left. It is not survivable now: `Ctrl-\` is a prefix, both
4031 clients filter it, and the outer keyboard has no spelling that reaches past the
4032 inner one. There is no escape chord left to document, so the loop is prevented
4033 rather than escaped, and the choice between "refuse" and "pass a chord through"
4034 was settled by that fact and not by taste.
4035
4036 Prevention needs the client to know where it is standing, which nothing on the
4037 wire could tell it: an attach describes a destination, not an origin. So the
4038 daemon plants the origin in the only channel a shell carries into every process
4039 it starts — its environment. `MUX_SOCK` is the path the daemon bound and
4040 `MUX_SESSION` is the session's RESOLVED name, `0` for the default rather than
4041 the empty spelling the wire uses, because the name a client compares against is
4042 the one it resolved too. `MUX_SOCK` rides the shared spawn plan, which is built
4043 once for every session this daemon will ever spawn; `MUX_SESSION` cannot, so
4044 `createSession` appends it per session. Both are planted after `extra_env`,
4045 inverting that field's usual last-word rule: a caller that could overwrite them
4046 could hand a shell a lie about where it is, and this is identity, not option.
4047
4048 The refusal is the exact self-pair and nothing wider. Same socket AND same
4049 session refuses; a different session of the same daemon is allowed, because it
4050 is useful and only the self-pair feeds its own paint back. A host or `quic://`
4051 target is never refused — a remote daemon is a different daemon however its
4052 sessions are named — and `--via` has nothing to compare. The socket comparison
4053 is string equality against the canonical path the daemon planted, so a
4054 symlinked or relative `--sock` spelling of the same socket evades it; that is
4055 accepted rather than fixed with a stat on every attach, because this guards
4056 against the mistake people make (typing `mux` in a mux shell), not against
4057 someone trying to get around it. An emptied variable counts as unset, so
4058 `MUX_SESSION=` overrides in a shell that cannot unset an exported name — the
4059 message names unsetting as the way out, and both spellings of that have to work.
4060
4061 Where the check sits is what keeps the chords working. It is in `mux_main`'s
4062 attach arm, after the default socket path is resolved — so a bare `mux`, the
4063 actual incident, is caught — and before the PATH search, the auto-start and the
4064 dial, none of which change the answer. `Ctrl-\ c/n/p` re-attach from inside
4065 `client.attach` and never pass back through that arm, so switching from session
4066 0 to session 1 is untouched by construction rather than by an exception.
4067
4068 `mux wall` refuses the WHOLE wall when a tile is the session it was launched
4069 from, naming the offending spelling. A tile is read-only, but it is still an
4070 attach, and a stripe of the session the wall lives in paints into the grid it
4071 is reading — the same loop with a nicer name. Dropping just that tile would be
4072 cheaper and worse: a wall that silently omits a session you asked for is a wall
4073 that lies, which is the objection already recorded against a truncated
4074 `sessions_reply` showing a partial wall.
4075
4076 The e2e leg for this deliberately has no `ptyclient`. A real client on a real
4077 pty is what the refusal PREVENTS, so the test injects one into the session
4078 shell with `muxa send` and reads the result out of a capture — no expect timing
4079 at all. Its money assertion is `"alt_screen":false` AFTER the injection, not
4080 the refusal message: before this milestone that field read true, because an
4081 inner client really had taken the screen, and a message grep alone would still
4082 pass for a client that printed the line and attached anyway. The daemon runs
4083 200 columns wide there, because every assertion greps a MESSAGE out of a grid
4084 dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`,
4085 split mid-sentence and a correct refusal read as a missing one.