Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #714 +/- ##
==========================================
+ Coverage 51.85% 52.14% +0.29%
==========================================
Files 25 26 +1
Lines 3645 3688 +43
Branches 735 741 +6
==========================================
+ Hits 1890 1923 +33
- Misses 1449 1461 +12
+ Partials 306 304 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tony
added a commit
that referenced
this pull request
Jul 11, 2026
why: A process running inside tmux can now ask libtmux where it is, which is new surface a reader deciding whether to upgrade wants to see. what: - Add a What's new entry for #714
This was referenced Jul 11, 2026
Closed
Open
tony
added a commit
that referenced
this pull request
Jul 12, 2026
why: A process running inside tmux can now ask libtmux where it is, which is new surface a reader deciding whether to upgrade wants to see. what: - Add a What's new entry for #714
tony
added a commit
that referenced
this pull request
Jul 12, 2026
why: A process running inside tmux can now ask libtmux where it is, which is new surface a reader deciding whether to upgrade wants to see. what: - Add a What's new entry for #714
bc2bab2 to
a2734b3
Compare
why: Server.__init__ tried to build socket_path from TMUX_TMPDIR, but the branch could never run: it required both `socket_name is None` and `socket_name != "default"`, and socket_name is defaulted to "default" two lines above. Dead since the fix that made a bare Server() reach the default socket. Guessing the path would have been wrong anyway. tmux resolves a -L name against its own socket directory, which honours TMUX_TMPDIR, the per-uid directory and the platform's overrides. Callers that need the real path ask tmux for it. what: - Remove the unreachable derivation from Server.__init__ - Pin the resulting contract: a named socket leaves socket_path None
why: tmux tells every process it spawns where it is, via two variables that no libtmux code read. Parsing them looks trivial and is not: $TMUX must be split from the right, because the socket path may contain a comma and the two fields after it are integers. And $TMUX_PANE's `%` sigil is load-bearing -- tmux's cmd_find routes a target to its pane slot by sigil, so a sigil-less value gets matched against session names and silently resolves to the wrong object. Only the socket path is taken from $TMUX. The session id it also carries is frozen at pane spawn and tmux never revises it, so it is provenance, not location. what: - Add exc.NotInsideTmux, naming the variable that was missing or malformed and why - Add libtmux._internal.env with socket_path_from_env and pane_id_from_env, both rejecting malformed input rather than guessing
…inside why: A process running inside tmux could not ask libtmux where it was. Callers hand-rolled it, and the obvious hand-roll is wrong: the session id in $TMUX is frozen at pane spawn, so it names the session the process was launched in, not the one holding it now. The from_env family reads only $TMUX_PANE and asks tmux for the rest, so the answer stays right after a move-window and matches what tmux itself would say for a window linked into several sessions. Server.from_env runs no tmux command at all -- it is a pure read of the environment. Window.from_env resolves through cls.from_window_id rather than traversing off the pane, so a subclass gets its own type back. what: - Add Server.from_env, Session.from_env, Window.from_env, Pane.from_env, each taking an optional env mapping so they are testable outside a pane - Cover the family with parametrized NamedTuple fixtures: bad ids, a stale session id, a linked window, move-window, the containing window vs the active one, a missing pane, a dead server, and subclass identity for all four constructors
why: The from_env family is only useful if you know it exists. An agent or a script running inside a pane is exactly the reader least likely to go looking for it, so it belongs on the front page and in its own topic. The doctest note in docs/AGENTS.md was also wrong in a way that costs time: it said the code blocks on a page share one session and that their order is load-bearing state. They do not. Each block is an independent pytest item with a fresh namespace and a fresh tmux server, which is why a block must be self-contained -- and why reordering prose is safe. what: - Add docs/topics/self_location.md: asking where you are, being outside tmux, the containing window vs the active one, why the server answers rather than the environment, linked windows, why there is no Client.from_env(), and how to test code that locates itself - Link it from the topics index and from traversal - Show it in the README and docs index examples - Define TMUX and TMUX_PANE in the glossary, and note that libtmux takes only the socket path from TMUX - Correct the doctest-isolation claim in docs/AGENTS.md, and list what conftest actually seeds into the namespace
why: Both said tmux writes -1 into TMUX for "a pane spawned without a session". A pane always has a session: spawn_pane dereferences s->id, s->name and s->options long before it builds the child environment, and TMUX_PANE is written in that path and nowhere else. The -1 is real, but it belongs to a different kind of child. environ.c's environ_for_session() writes -1 only when it is handed no session, and the one caller that passes none is format.c's #() job. run-shell gives job_run its target session, so its children carry a real session id. Neither kind of job gets TMUX_PANE -- so a process holding a pane id always has a real session id beside it. Nothing branched on the sentinel, so this is a docs fix, not a behaviour change. what: - Attribute the -1 sentinel to #() jobs, not to run-shell - Say that TMUX_PANE excludes job children either way - Drop the -1 claim from the TMUX glossary entry
why: A process inside tmux could not ask libtmux where it was, and the obvious hand-roll reads a session id that tmux freezes at spawn and never revises. what: - Name the four from_env constructors and the env= seam that keeps code locating itself testable outside a pane - State the guarantees: survives move-window, names the session tmux would act on, raises NotInsideTmux rather than guessing - Send the mechanism to the self-location topic page
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #704 (addresses its self-location half; the papercuts in its §3 are untouched and still want owners). Supersedes #705.
Stacked on #713 — review that first; this PR's diff is only its own six commits. It cannot answer "which session am I in?" correctly without the resolution fix that #713 lands.
The problem
A process running inside tmux — a script in a split, a hook, a test harness, an agent — could not ask libtmux where it was. It had to hand-roll the answer, and the obvious hand-roll is wrong.
tmux exports two variables into every pane it spawns (
environ.c:277-282,spawn.c:325):The trap is
session_id. tmux writes these once, at spawn, and never revises them — grep 3.7b for every other reference and you find only reads. So that session id records where the process was launched, not where it is. Move the pane's window to another session and the id is simply wrong; the session may not even exist any more.The parse is a trap too.
split(",")splits from the left, so a socket path containing a comma — which tmux permits, via-S,-LorTMUX_TMPDIR— shears into garbage. The pid and session id are integers, so every comma but the last two belongs to the path: the only correct parse isrsplit(",", 2).And if you do read the session id, tmux spells it bare (
47) where libtmux spells the same session$47, so it needs normalising before it is good for anything. The conclusion this PR draws is simpler: don't read it.The API
Each takes an optional
envmapping (defaulting toos.environ) so it is testable without a real pane.from_envreads only the socket path out ofTMUX, anchors onTMUX_PANE, and asks tmux for everything else. So the answer stays correct after amove-window, and for a window linked into several sessions it is the session tmux itself would act on — because tmux is what resolved it.Server.from_env()runs no tmux command at all; it is a pure read of the environment.The session id in
TMUXis never consulted. Not as a fallback, not as a tie-break.Outside a pane, or with a
TMUXthat does not parse, the family raisesNotInsideTmuxrather than guessing.Two details worth a reviewer's eye
Window.from_envresolves throughcls.from_window_id, not by traversing off the pane.return Pane.from_env(env).windowwould have been shorter and would silently hand a baseWindowback to a subclass.test_from_env_returns_the_class_it_was_called_onpins this for all four constructors.Session.from_envmirrors it throughcls.from_session_id, and both raise the sameValueErrorwhen the resolved pane names no parent — the guard that stands in for theassertapython -Orun strips.pane_id_from_envrejects aTMUX_PANEwithout its%sigil. The sigil is load-bearing: tmux'scmd_findroutes a target to its pane slot by sigil, so a sigil-less value is matched against session names and silently resolves to the wrong object. Guessing here produces a wrong answer with no error, so it raises instead.Also in this PR
Server.__init__derivedsocket_pathfromTMUX_TMPDIRin a branch that could never execute: it required bothsocket_name is Noneandsocket_name != "default", andsocket_nameis defaulted to"default"two lines above. Removed. Guessing the path would have been wrong anyway — tmux resolves a-Lname against its own socket directory, honouringTMUX_TMPDIR, the per-uid directory, and platform overrides. Callers that need the real path ask tmux (#{socket_path}).The
-1session id is documented for what it actually is. tmux writes it only when it builds an environment with no session, and the sole caller that passes none is the#()format job (format.c:417→job_run(..., NULL, ...), andenviron.c'senviron_for_session()writes-1exactly whens == NULL).run-shellhandsjob_runits target session (cmd-run-shell.c:208), so its children carry a real session id. Neither kind of job child getsTMUX_PANE—spawn.c:325is the only place tmux sets it — so a process holding a pane id always has a real session beside it, and-1can never reachfrom_env.Not in this PR
Reading the spawn session — "which session launched me?", as distinct from "which session am I in?" — is a real question with a real audience, and it is deliberately out of scope here. Proposed separately in #712.
There is no
Client.from_env(): a pane is not owned by a client, tmux exports no client id into one, and there may be zero clients attached or several. See the topic page.Docs
New topic page,
self_location, linked from the topics index and from traversal, plus front-page examples in the README and docs index, andTMUX/TMUX_PANEin the glossary.It also corrects a claim in
docs/AGENTS.mdthat costs real time: it said the code blocks on a docs page share one doctest session and that their order is load-bearing state. They do not — every fenced block is an independent pytest item with a fresh namespace and a fresh tmux server. (I re-proved this by walking straight into it while writing the new page.)Verification
ruff·ruff format·mypy·pytest --reruns 0·just build-docs— all green locally, and CI is green across the full matrix (3.2a→master).Tests use parametrized
NamedTuplefixtures withtest_id, and cover the error paths as well as the happy one: malformedTMUX, a sigil-lessTMUX_PANE, a comma in the socket path, a stale session id, a linked window,move-window, the containing window vs the active one, a pane that names no parent, a missing pane on a live server, a dead server, and subclass identity for all four constructors.