From 81f8c8e894c56928ac6ea0346364025ae13f0922 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 6 Nov 2026 03:49:59 +0100 Subject: [PATCH] fix: the live transcript lookup targets the exact tmux session (release review) list-panes -t '=name' with no colon is a window target to tmux, so a remote session that is gone resolved to a window of that name or to a session the name is a prefix of, and the Edit history panel moved to that session's conversation and stored its diffs. `=name:` is an exact session match, as src/remote-pane-history.ts already uses. Measured on tmux 3.4. The fake tmux in the test matched the name as a substring, so it passed with or without the colon. It now needs the exact token and answers a bare prefix the way real tmux does. Co-Authored-By: Claude Opus 5.5 --- src/remote-files.ts | 6 +++++- test/remote-edit-sources.test.ts | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/remote-files.ts b/src/remote-files.ts index 765b953e..1a006d76 100644 --- a/src/remote-files.ts +++ b/src/remote-files.ts @@ -522,12 +522,16 @@ function claudeConfigDirLines(): string[] { * non-interactive shell may not list the directory it lives in, and a silent miss here leaves * the panel on the launch conversation after a `/clear`. The `export` stays inside the `$(…)`, * and inside this script, which runs under `sh` whatever the remote user's login shell is. + * + * ⚠️ `=name:` is an EXACT session match. Without the colon tmux reads a window target, so a + * session that is gone resolves to a window of that name or a session the name is a prefix of, + * and that session's conversation would land in this one's edit history. */ function liveClaudeIdLines(pane: RemoteClaudePane | undefined): string[] { if (!pane || !TMUX_NAME_PATTERN.test(pane.socket) || !TMUX_NAME_PATTERN.test(pane.sessionName)) return []; return [ 'live=', - `pp=$(${withRemoteTmuxPath(`tmux -L ${shellescape(pane.socket)} list-panes -t ${shellescape(`=${pane.sessionName}`)} -F '#{pane_pid}' 2>/dev/null | head -n 1`)})`, + `pp=$(${withRemoteTmuxPath(`tmux -L ${shellescape(pane.socket)} list-panes -t ${shellescape(`=${pane.sessionName}:`)} -F '#{pane_pid}' 2>/dev/null | head -n 1`)})`, "case $pp in ''|*[!0-9]*) pp= ;; esac", 'if [ -n "$pp" ]; then', ' tree=$(ps -A -o pid= -o ppid= 2>/dev/null)', diff --git a/test/remote-edit-sources.test.ts b/test/remote-edit-sources.test.ts index 11795a77..ca3b6de6 100644 --- a/test/remote-edit-sources.test.ts +++ b/test/remote-edit-sources.test.ts @@ -173,9 +173,20 @@ describe('the transcript lookup on a real shell', () => { bin = mkdtempSync(join(tmpdir(), 'codeman-fake-tmux-')); const localBin = join(home, '.local', 'bin'); mkdirSync(localBin, { recursive: true }); + // Only the exact `=name:` target reaches the pane. Real tmux (3.4, measured) reads `=name` + // without the colon as a window target too, so it also answers for a window of that name + // or a session the name is a prefix of: the second pattern is that second case. writeFileSync( join(localBin, 'tmux'), - '#!/bin/sh\ncase "$*" in *"=codeman-ssh-conv1abc"*) echo 100 ;; *) echo "no such session" >&2; exit 1 ;; esac\n' + [ + '#!/bin/sh', + 'for a; do', + ' case "$a" in =codeman-ssh-conv1abc:|=codeman-ssh-conv1) echo 100; exit 0 ;; esac', + 'done', + 'echo "no such session" >&2', + 'exit 1', + '', + ].join('\n') ); writeFileSync(join(bin, 'ps'), '#!/bin/sh\nprintf " 1 0\\n100 1\\n200 100\\n300 200\\n"\n'); chmodSync(join(localBin, 'tmux'), 0o755); @@ -210,6 +221,12 @@ describe('the transcript lookup on a real shell', () => { expect(locateIn(pane)).toBe(join(projects(), 'new', 'conv-1.jsonl')); }); + it('never takes the pane of another tmux session whose name starts with this one', () => { + // codeman-ssh-conv1 is gone, codeman-ssh-conv1abc is alive: its conversation is not ours. + session('{"pid":300,"sessionId":"solo"}'); + expect(locateIn({ ...pane, sessionName: 'codeman-ssh-conv1' })).toBe(join(projects(), 'new', 'conv-1.jsonl')); + }); + it('reads the live id from the sessions dir under CLAUDE_CONFIG_DIR', () => { const cfg = join(home, 'alt-config'); mkdirSync(join(cfg, 'sessions'), { recursive: true });