clikae v0.24.0 — the design doc specified a function that was never written
The design doc specified a function. Nobody wrote it.
docs/DESIGN-tmux.md has been the single source of truth for this layer since
v0.4 — six rules, each with receipts. Rule 2 asks for one shared set of exits.
Rule 5 refers to a wrapper called clikae_spawn_session as "Rule 1 與 Rule 2 的
封裝函式".
That function appeared three times in the design doc and zero times in the
source. So four call sites — switch.sh ×3, burn.sh ×1 — each
re-implemented the rules by hand, and drifted exactly the way Rule 2 predicted.
Every fix below is that one omission.
lib/core/tmux.sh is now the only tmux new-session in the codebase.
What was silently wrong
A server born by clikae burn carried none of clikae's global options.
burn.sh used a bare tmux new-session -d, without the 200-character option
prefix the other three sites copied between themselves. Measured on a server
created that way:
$ tmux show-options -gv history-limit
2000 # tmux's default. The intended value is 50000.
It went unnoticed because the next clikae switch repaired it. The window is
only visible while the burn's own server is still alive — exit-empty means
asking a dead server for its options quietly starts a fresh one that answers
with the defaults, which reads identically to the bug whether or not the bug is
there.
clikae burn published your environment to ps. It passed all of
compgen -e to tmux new-session as -e KEY=VAL pairs, and those pairs stay in
the tmux process's argv — the server's argv, when the burn is what created it,
for as long as that server lives. Verified on a server born two days earlier: its
command line still listed every pair it was created with. The environment now
travels in burn's wrapper script, created and chmod 0600'd before anything is
written to it.
A carried session could be handed an SSH agent socket that was never created.
The dry-tank carry path passed clikae's stable symlink path without the
ln -sf that creates it. The interactive path did both. Now one function does.
Selecting and copying text works again
Reported as "since clikae started using tmux I cannot copy text" — and the
diagnosis is that the text was never unselectable. It was unreachable, and
clikae put it out of reach.
Rule 1's *:smcup@:rmcup@ disables the outer terminal's alternate screen,
which the scrollback capture needs. The cost was never counted: the outer
terminal's scrollback then fills with tmux's full-screen redraws, so the wheel
scrolls debris while the clean 50000-line history sits in tmux where the wheel
cannot reach it. And tmux's default set-clipboard external forwards an
application's own OSC 52 but never emits one for tmux's own selections, so even
entering copy-mode and yanking put the text in a buffer only tmux could paste.
mouse on puts the wheel and the drag onto tmux's real history;
set-clipboard on puts a copy-mode yank on the system clipboard. The cost,
stated rather than hidden: a native terminal selection — for pasting somewhere
tmux is not — now needs the option key held.
Found while measuring it: those options are appended, and the option block
ran on every session creation rather than only at server birth. A two-day-old
server carried four identical *:smcup@:rmcup@ entries and four
xterm*:extkeys. Harmless to tmux, and the same shape as everything else here —
an operation written as though it were idempotent when it is really cumulative.
Rule 7: a server keeps what it was born with
A Soul kept under ~/Library/Mobile Documents became unreadable to every tank on
one tmux server and stayed readable on another. The only symptom was EPERM,
with no prompt and nothing in any log.
tests/bats/roam.bats had already written half of this down —
everything else is inherited from the SERVER's process environment — which is
whoever started the server, not us
— and fixed it for environment variables by passing them explicitly with -e.
The other half cannot be fixed that way: on macOS the server also inherits its
file-access identity, and nothing can hand that over after birth.
So soul_prelaunch now probes the memory it is about to hand over. The test is a
two-syscall asymmetry rather than an errno:
| measured | meaning |
|---|---|
stat succeeds, read fails |
the path is right and still yields nothing |
| the permission bits allow the read, and it fails anyway | something above the filesystem refused — the server, not a chmod |
| the bits deny it | an ordinary permissions problem, reported as one |
[ -r ] calls access(2), which reads only the bits and answers "yes" in the
first case. Only an actual read tells the truth.
It warns and starts anyway. A session with no memory is bad; a tank that will not
start is worse.
And tmux_spawn_session now records what the server was born from
(tmux show-environment -g CLIKAE_SERVER_BORN), because by the time it matters
the parent is always launchd: the server at the centre of this could not be
traced to either the interactive or the unattended path, since both leave
byte-identical command lines.
Running the test suite no longer kills your live tanks
tests/bats/roam.bats runs a bare tmux kill-server twice — it needs a
known-empty server to prove create-or-attach — and the suite had no tmux
isolation of any kind. On the default socket that reaches every clikae tank the
person running the tests has open. scripts/test.sh was unsafe to run on any
machine that dogfoods clikae, which is every machine that runs it.
Two parts, because the obvious one is not enough:
TMUX_TMPDIR=<iso> tmux list-sessions -> the isolated server
TMUX=<real> TMUX_TMPDIR=<iso> tmux list-sessions -> the four live tanks
An inherited $TMUX overrides TMUX_TMPDIR, and anyone running the tests from
a tmux pane — the normal way — has it set. The suite now unsets TMUX and
TMUX_PANE as well, and keeps a negative control proving the unset is
load-bearing rather than decorative. tests/tools/pty-smoke.py needed the same
treatment for a different reason: it never invokes tmux, but it drives clikae on
a real pty, so clikae really does create sessions.
Verifying it
scripts/verify-tmux-birth.sh covers what bats cannot reach — properties of the
server hosting a session, on a real machine. Read-only and idempotent. Its first
check is whether the installed clikae is even the one with the tmux layer,
because every later check would otherwise measure the old build and pass for the
wrong reason.
It reports three states, and so does the probe it tests: a check it could not
perform is skip, never a pass. The class of bug being guarded against
throughout this release is a green light that means "I did not look".
scripts/test.sh: 753 passing, 0 failing.