Skip to content

fix(core): learn a pane's window when it is created, not after - #1115

Merged
LeTuR merged 1 commit into
Thurbeen:mainfrom
spscream:fix/window-id-at-birth
Sep 13, 2026
Merged

LeTuR merged 1 commit into
Thurbeen:mainfrom
spscream:fix/window-id-at-birth

Conversation

@spscream

Copy link
Copy Markdown
Contributor

Your nit on #1107, taken up: new-window is already asked to answer, so it may as well answer with the window too.

The defect this closes

tmux announces a pane's death only by closing its window, so pane_windows is what lets a pane notice its own ending. That mapping was built by asking: a display-message -t %N -p '#{window_id}' round trip, issued after new-window had already returned, travelling the serialized control-mode channel behind whatever else was in flight. A program that ended inside that gap had its %window-close arrive with nothing to match it against — and the announcement is one-shot, so no later wait brings it back.

register_pane's own comment called that "narrow and deliberately not paid for", on the grounds that closing it meant a second round trip per pane. That was the wrong price: it takes none. new-window is asked with -P -F; '#{pane_id} #{window_id}' costs the same round trip it already spends, and the mapping is then written from the answer that created the pane.

adopt is handed a pane id out of the database and nothing else, so it keeps asking exactly as before — as does any multiplexer that answers with the pane id alone (psmux's -P -F support is unverified against ADR-13, so it falls through the same path it does today).

Measured, not reasoned

The mechanism, A/B. A 1.5 s delay inserted in the display-message path — standing in for a round trip queued under load — and a program shortened to sleep 0.2 so it ends inside it. Same test, same instrumentation, one difference:

path result
A window id arrives with the pane id passed, 0.76 s (the delayed branch is never taken)
B window id asked for separately (today's main) FAILEDresize-window -t %2 -x 80 -y 23: can't find pane: %2

Being honest about what that shows: B fails loudly, because the pane is gone before connect_pane finishes attaching. I could not reproduce the silent-timeout shape you hit in CI, and without your run's logs I will not claim this is the same surface — what is measured is that a program ending inside that round trip breaks the path, and that there is no longer a round trip to end inside.

The suite, before and after. Six full parallel cargo nextest run --all runs each, this machine, tmux 3.2a:

  • baseline (this branch's timings, main's code): the two program tests passed 6/6
  • with the fix: 6/6, 2643 tests, the same four pre-existing environment failures in both columns (shared_tests::a_host_with_sharing_off_is_used_the_old_way, spawn::tests::resolve_host_accepts_the_backend_name_the_interface_carries, cli::automations::tests::tick_reports_fired_and_skipped_arrays, shared_sessions::sync_with_no_shareable_host_configured_is_an_empty_report — all four reproduce on main here)

So the flake never reproduced on this machine, at either timing. The suite runs are evidence of no regression, not evidence of the fix; the A/B above is the evidence of the fix.

The timings from #1113 go back

sleep 8 and the 20 s deadline in program_pane_exit.rs, sleep 3 in program_restart_exit.rs. They were raised twice to buy room for the round trip that is now gone — and Greptile's note on #1114 is right that 3 s was never consistent with 8 s anyway. Their comments are rewritten to say what the second is actually for.

What this does not do — and a question

The gap is now local work rather than a queued round trip, but it is not zero: the mapping is still written inside register_pane, a few instructions after new-window returns, and the reader thread could in principle process %window-close in between.

Closing it completely means remembering recently-closed window ids — a small bounded set in the control-mode state — and checking it at registration, so a pane that registers after its window has already gone gets EOF straight away. That is deterministic, and it is what Greptile asked for on #1114 in as many words. It is also new state in control mode rather than your nit, so I have left it out and would rather you ruled on it: worth it here, a separate PR, or not at all?

The other half I deliberately left alone: the adopt side could carry #{window_id} in DISCOVER_FORMAT and window_panes for free, but it buys nothing until the id is threaded through restore to adopt, which is a different layer. Say if you want it in the same change.

If you would rather keep the wider margins and leave the round trip where it is, say so and I will close this.

tmux announces a pane's death only by closing its window, so a pane that
does not know which window it is in cannot notice its own ending. That
mapping was built by asking: a `display-message` round trip issued after
`new-window` had already returned, travelling a serialized control-mode
channel behind whatever else was in flight. A program that ended inside
that gap had its `%window-close` arrive with nothing to match it
against, and the announcement is one-shot — no later wait brings it
back.

`new-window` is already asked to answer (`-P -F`), and asking it for the
window as well as the pane costs nothing. The mapping is then written
from the answer that created the pane, with no gap to lose a death in.
`adopt` is handed a pane id out of the database and nothing else, so it
keeps asking as before; so does a multiplexer that answers with the pane
id alone.

Measured on tmux 3.2a, with a 1.5s delay standing in for a round trip
queued under load and a program that ends inside it: asking separately
fails the spawn outright (`can't find pane: %2`), answering with both
ids passes. The timings Thurbeen#1113 raised to buy room for that round trip go
back to what they were written as — `sleep 8` and a 20s deadline here,
`sleep 3` next door.
@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR obtains both pane and window IDs from the existing new-window -P -F response, validates the window ID, and passes it into pane registration to remove a separate display-message round trip. It also restores shorter timings in two pane-exit tests.

  • Adds validation for tmux window identifiers.
  • Uses #{pane_id} #{window_id} when spawning a window while retaining the lookup fallback for adopted or compatibility paths.
  • Reduces the pane-exit test process lifetime and deadline.
  • The mapping is still installed only after the creation response returns, leaving immediately exiting panes exposed to the same permanent lost-close state.

Confidence Score: 4/5

The PR should not merge until immediately exiting panes cannot lose their one-shot window-close notification before registration.

Removing the extra lookup substantially narrows the race, but new-window still starts the child before the returned window ID is inserted into pane_windows; an immediate exit can consequently leave the pane permanently marked as running.

Files Needing Attention: src/agent/tmux.rs

Important Files Changed

Filename Overview
src/agent/tmux.rs Threads the window ID from new-window into registration, but registration still occurs after the child has started and the command response has returned.
src/agent/control_mode/mod.rs Adds strict @<digits> window-ID validation; existing unmatched window-close handling exposes the remaining registration race.
tests/program_pane_exit.rs Restores a one-second child lifetime and ten-second deadline without deterministically covering an exit before registration.
tests/program_restart_exit.rs Restores a one-second child lifetime for restart/exit coverage.

Sequence Diagram

sequenceDiagram
    participant Main as Spawn thread
    participant Tmux
    participant Reader as Control reader
    participant Map as pane_windows

    Main->>Tmux: new-window -P -F pane_id window_id
    Tmux->>Tmux: Start child
    alt Child exits before response is registered
        Tmux-->>Reader: "%window-close @N"
        Reader->>Map: "Find panes mapped to @N"
        Map-->>Reader: None
        Reader->>Reader: Discard one-shot close
    end
    Tmux-->>Main: "%N @N"
    Main->>Map: "Register %N → @N"
    Note over Main,Map: Too late if the close was already discarded
Loading

Reviews (1): Last reviewed commit: "fix(core): learn a pane's window when it..." | Re-trigger Greptile

Comment thread src/agent/tmux.rs

@LeTuR LeTuR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the supporting fix in the split #1107's review asked for. Nothing in it blocks from my side; merging is LeTuR's call.

  • tmux 3.2 floor: checked on real tmux 3.2 (built from the release tarball) and 3.5a in control mode. -P -F '#{pane_id} #{window_id}' answers %N @M, and a window named 'x %9 @9 y' does not change that answer.
  • #1107 retention: both set-window-options still run in the same command list as new-window. On 3.2, show-window-options on the new window reads remain-on-exit off and window-size manual.
  • CI on ed80bfa8: 13 of 22 checks passed and 9 were skipped by path filters (lint, install scripts, website, SonarQube). None failed, was cancelled or was held. Nextest ran 2643 tests with 0 skipped, both program-exit tests included, on Ubuntu's tmux 3.4. No CI job runs tmux 3.2, which is why I checked it by hand. psmux was not tested.
  • Security: no security findings. The format is a constant with no names in it, both ids are validated before use, and the new check on the display-message answer tightens the adopt path.
  • Your two questions, my recommendation with LeTuR deciding: the recently-closed set may not be needed on the spawn path (see the Greptile thread). Threading the window id through DISCOVER_FORMAT and restore belongs in a separate change.

— LeTuR's agent

Comment thread src/agent/tmux.rs
Comment thread tests/program_pane_exit.rs
@LeTuR
LeTuR merged commit 116fdb7 into Thurbeen:main Sep 13, 2026
22 checks passed
@spscream
spscream deleted the fix/window-id-at-birth branch September 13, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants