fix(tmux): target windows by id, not by a name tmux reads as an index #105

Merged
lz merged 3 commits from fix/tmux-window-targeting into main 2026-09-04 17:29:06 +02:00
Owner

Independent of the #96 stack (branched off main), but included in the release candidate — these are live bugs that a production trial would hit.

The bug

tmux resolves the window half of a session:window target as an INDEX first, falling back to a name match only if that fails. Session names are git refs, and 1 / 42 are legal refs the create form accepts.

Measured on tmux 3.3a with windows 0:workspace 1:alpha 2:"1":

nexus:1     ->  @1 alpha      WRONG
nexus:=1    ->  @1 alpha      WRONG  — the '=' exact-match prefix does NOT help
nexus:@2    ->  @2 "1"        correct

send-keys -t nexus:1 -l TEXT  ->  delivered into alpha's pane
send-keys -t @2      -l TEXT  ->  delivered into "1"'s pane

Three call sites, not two

  • artifacts/tmux-nudge.ts — an operator nudge aimed at the session named 1 was typed into another agent's claude prompt, and Enter pressed. The list-windows call that already verified the window now also carries #{window_id}.
  • terminal/exec-pty.ts — the browser terminal attached to and displayed the wrong session's pane, so the operator typed into an agent they had not picked. VIEW:@id still leaves the original nexus session's own active-window pointer untouched, so the grouped-session shape is unchanged.
  • sessions/deleteSession — found by the review pass, and the most destructive: kill-window -t nexus:1 killed alpha, SIGHUPing another agent's claude, while the session actually being deleted kept running with an orphaned window.

Two silent failures, also fixed

  • A failed list-windows was indistinguishable from an absent window: tmux being unreachable reported no tmux window named "alpha", pointing the operator at a session that is fine.
  • sendNudge claimed delivery on a failed send. execInContainer resolves with a non-zero exitCode rather than throwing, so the original try/catch could never see it — and that value surfaces as prompt_typed: true in the submit API. The operator was told their feedback reached the agent when nothing had.

Plus reject(sock, 500, …) no longer labels itself HTTP/1.1 500 Bad Request.

Guard

tmux-window.test.ts scans src/lib/server/**/*.ts and fails on any nexus:${...} target that is not the fixed WORKSPACE_WINDOW constant (precedent: auth/sealed-columns.test.ts). Negative-controlled — a planted offender fails the test naming the file and the expression. Stated limitation: 'nexus:' + name slips past.

Also measured and recorded: tmux never reuses a window id. Kill @1, create a window, you get @3; -t @1 then fails rather than hitting a bystander. That is what makes resolve-then-use safe across the gap between the two execs.

Merge note

Conflicts with the #96 stack in sessions/service.ts and service.test.ts — both fix deleteSession, differently. Resolve by deleting resolveSessionWindowId in favour of the shared lib/tmux-window.ts. Resolved and gated in the release-candidate branch.

Gates

109 files / 1030 tests passed
typecheck  4856 FILES  0 ERRORS  0 WARNINGS
lint       clean
Independent of the #96 stack (branched off `main`), but included in the release candidate — these are live bugs that a production trial would hit. ## The bug tmux resolves the window half of a `session:window` target as an **INDEX first**, falling back to a name match only if that fails. Session names are git refs, and `1` / `42` are legal refs the create form accepts. Measured on tmux 3.3a with windows `0:workspace 1:alpha 2:"1"`: ``` nexus:1 -> @1 alpha WRONG nexus:=1 -> @1 alpha WRONG — the '=' exact-match prefix does NOT help nexus:@2 -> @2 "1" correct send-keys -t nexus:1 -l TEXT -> delivered into alpha's pane send-keys -t @2 -l TEXT -> delivered into "1"'s pane ``` ## Three call sites, not two - **`artifacts/tmux-nudge.ts`** — an operator nudge aimed at the session named `1` was **typed into another agent's claude prompt, and Enter pressed.** The `list-windows` call that already verified the window now also carries `#{window_id}`. - **`terminal/exec-pty.ts`** — the browser terminal attached to and displayed the **wrong session's pane**, so the operator typed into an agent they had not picked. `VIEW:@id` still leaves the original `nexus` session's own active-window pointer untouched, so the grouped-session shape is unchanged. - **`sessions/deleteSession`** — found by the review pass, and the most destructive: `kill-window -t nexus:1` **killed alpha**, SIGHUPing another agent's claude, while the session actually being deleted kept running with an orphaned window. ## Two silent failures, also fixed - A failed `list-windows` was indistinguishable from an absent window: tmux being unreachable reported `no tmux window named "alpha"`, pointing the operator at a session that is fine. - `sendNudge` claimed delivery on a failed send. `execInContainer` resolves with a non-zero `exitCode` rather than throwing, so the original `try/catch` could never see it — and that value surfaces as `prompt_typed: true` in the submit API. The operator was told their feedback reached the agent when nothing had. Plus `reject(sock, 500, …)` no longer labels itself `HTTP/1.1 500 Bad Request`. ## Guard `tmux-window.test.ts` scans `src/lib/server/**/*.ts` and fails on any `nexus:${...}` target that is not the fixed `WORKSPACE_WINDOW` constant (precedent: `auth/sealed-columns.test.ts`). Negative-controlled — a planted offender fails the test naming the file and the expression. Stated limitation: `'nexus:' + name` slips past. Also measured and recorded: **tmux never reuses a window id.** Kill `@1`, create a window, you get `@3`; `-t @1` then fails rather than hitting a bystander. That is what makes resolve-then-use safe across the gap between the two execs. ## Merge note Conflicts with the #96 stack in `sessions/service.ts` **and** `service.test.ts` — both fix `deleteSession`, differently. Resolve by deleting `resolveSessionWindowId` in favour of the shared `lib/tmux-window.ts`. Resolved and gated in the release-candidate branch. ## Gates ``` 109 files / 1030 tests passed typecheck 4856 FILES 0 ERRORS 0 WARNINGS lint clean ```
lz added 2 commits 2026-09-04 00:07:55 +02:00
tmux resolves the window half of a `session:window` target as an INDEX
first, falling back to a name match only if that fails. Session names are
git refs, and `1` / `42` are legal refs the create form accepts, so both
remaining name-based targets could act on a different session's window.

Measured on tmux 3.3a with windows `0:workspace 1:alpha 2:"1"`: `nexus:1`
and `nexus:=1` both resolve to **alpha** (the `=` exact-match prefix does
not help), while `@2` is the window actually named `1`. `send-keys -t
nexus:1 -l TEXT` was delivered into alpha's pane.

- artifacts/tmux-nudge.ts: an operator nudge aimed at the session named
  `1` was typed into another agent's claude prompt, and Enter pressed. The
  list-windows call that already verified the window now also carries
  `#{window_id}`, and the sends target `@N`. The
  `{delivered:false, reason:'window not found'}` contract is unchanged.
- terminal/exec-pty.ts: the browser terminal attached to and displayed the
  wrong session's pane, so the operator typed into an agent they had not
  picked. `buildTmuxAttachArgv` stays a pure argv builder and now takes
  the id; `openTerminalPty` resolves it (one extra exec per terminal-open,
  not a poll path) and rejects when no window carries the name rather than
  falling back — handleTerminalUpgradeCore already answers an openPty
  rejection with `500 failed to open terminal`, so no new failure mode.

`VIEW:@ID` still leaves the original `nexus` session's own active-window
pointer untouched, so the grouped-session shape is otherwise unchanged.

lib/tmux-window.ts holds the shared `-F` format and its parser so the
measurement is documented once instead of at each call site.
fix(tmux): kill windows by id too, and stop reporting tmux failures as missing windows
All checks were successful
ci / nexus (pull_request) Successful in 6m40s
ci / images (pull_request) Successful in 7m39s
135b3a75b6
Review pass over c42159c. Three findings.

1. `deleteSession` still built `nexus:${row.name}` for its `kill-window`.
   Measured on tmux 3.3a with windows `0:workspace 1:alpha 2:"1"`: deleting
   the session named `1` ran `kill-window -t nexus:1` and killed **alpha** —
   another agent's claude, SIGHUPed — while the session being deleted kept
   running. Same defect class the commit fixes, one call site short. It now
   resolves the id first and kills `@N`, or nothing if the window is gone.

2. A failed listing was indistinguishable from an absent window. Both call
   sites read a non-zero `list-windows` exit as "no such window":
   `openTerminalPty` threw `no tmux window named "alpha"` when tmux was not
   answering at all, and `sendNudge` returned `window not found` — pointing
   the operator at a session that is fine. `resolveWindowId` now throws with
   tmux's own exit code and stderr; the nudge reports `cannot reach tmux`.

3. `sendNudge` claimed delivery on a failed send. `execInContainer` resolves
   with a non-zero exitCode rather than throwing, so a window that died
   between the listing and the send-keys produced `delivered: true` — the
   artifact flips to submitted with nothing typed into any pane.

Also: the shared `resolveWindowId` moved into lib/tmux-window.ts so both
container-side callers use one implementation; a source-scan test fails on any
new `nexus:${...}` target that isn't the fixed `WORKSPACE_WINDOW` constant;
`reject()` no longer labels a 500 `Bad Request`; and the rationale duplicated
at each call site is trimmed back to the module that holds the measurement.

Measured and added to the module doc: tmux never reuses a window id (killing
`@1` then creating a window gives `@3`, and `-t @1` fails rather than hitting a
bystander), so resolve-then-use cannot misfire on a stale id.
lz referenced this pull request from a commit 2026-09-04 00:15:25 +02:00
merge: main into fix/tmux-window-targeting
All checks were successful
ci / nexus (pull_request) Successful in 14m1s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 11s
ci / images (pull_request) Successful in 10m47s
642ec8934e
Conflicts in sessions/service.ts and service.test.ts, both caused by the #96
stack rewriting the same call sites this fix retargets. Resolved by taking the
form already committed and gated inside rc/sidebar-96: `resolveSessionWindowId`
is deleted in favour of the shared `resolveWindowId`, and both test suites are
kept.
lz merged commit 156e642acf into main 2026-09-04 17:29:06 +02:00
Sign in to join this conversation.
No reviewers
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!105
No description provided.