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 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 03:49:59 +01:00
1 parent 08d99da5ae
commit 81f8c8e894
2 files changed
+23 -2

No files matched your search

+5 -1
View File
@@ -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)',
+18 -1
View File
@@ -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 });