mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(mux): treat the launch directory as no answer for a discovered pane
Every pane starts in TMUX_LAUNCH_CWD (/tmp) and reaches its workspace with a cd inside the pane command, but a remote or docker pane never does that cd, so tmux reports /tmp for it. A rediscovered record carries no remote/docker metadata and looks local, so adopting that directory would root the session at /tmp: hooks written into /tmp/.claude, the Files panel at /tmp, and a respawn running claude in a directory any local account can write to. reconcileSessions() now skips a pane whose directory is the launch directory, in both spellings a pane can report it: the literal one and its realpath (/private/tmp on macOS, where a process cwd comes back resolved). The set is computed once at module load. Such a session falls back to process.cwd(), as before this series. Also stop trimming the list-panes output: the directory is the last field, so a trim cut a trailing space off the last row's path; parsePaneRows() already skips empty lines. And the comments that list what a discovered record knows for certain now include the directory, when tmux reported a usable one. Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
3bd7e81233
commit
ced88b3310
3 files changed
+82
-6
No files matched your search
@@ -63,7 +63,8 @@ export interface MuxSession {
|
||||
statusLine?: StatusLineExporter;
|
||||
/**
|
||||
* This record was rebuilt from the tmux socket rather than from Codeman's own
|
||||
* bookkeeping, so everything on it but the name and the pid is a guess. Its
|
||||
* bookkeeping, so everything on it but the name, the pid and, when tmux
|
||||
* reported a usable one, the directory is a guess. Its
|
||||
* synthetic `restored-<fragment>` id cannot find the session's `state.json`
|
||||
* entry either, which means a remote or docker session rediscovered this way
|
||||
* arrives with no `remote`/`docker` metadata and looks local. Anything that
|
||||
|
||||
+31
-4
@@ -27,7 +27,7 @@ import { execSync, exec, execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, mkdirSync, realpathSync } from 'node:fs';
|
||||
import { writeFile, rename } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
@@ -183,6 +183,22 @@ const DEFAULT_REMOTE_RECONNECT_INTERVAL_MS = 5000;
|
||||
/** Stable cwd for tmux server/pane launch; actual session cwd is reached inside the pane. */
|
||||
const TMUX_LAUNCH_CWD = '/tmp';
|
||||
|
||||
/**
|
||||
* TMUX_LAUNCH_CWD in every spelling a pane can report it. tmux reads a pane's
|
||||
* directory from its process, which comes back resolved (`/private/tmp` on
|
||||
* macOS, where `/tmp` is a symlink), so the literal and the resolved spelling
|
||||
* both count. Computed once.
|
||||
*/
|
||||
const TMUX_LAUNCH_CWD_SPELLINGS: ReadonlySet<string> = (() => {
|
||||
const spellings = new Set([TMUX_LAUNCH_CWD]);
|
||||
try {
|
||||
spellings.add(realpathSync(TMUX_LAUNCH_CWD));
|
||||
} catch {
|
||||
// Unresolvable launch directory: the literal spelling is all there is.
|
||||
}
|
||||
return spellings;
|
||||
})();
|
||||
|
||||
/** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
|
||||
export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
|
||||
|
||||
@@ -3128,14 +3144,24 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
const output = execSync(`${this.tmux()} list-panes -a -F '${PANE_LIST_FORMAT}' 2>/dev/null || true`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUT_MS,
|
||||
}).trim();
|
||||
});
|
||||
// Not trimmed: the directory is the last field, so trimming would cut a
|
||||
// trailing space off the last row's path. parsePaneRows skips empty lines.
|
||||
const rows = parsePaneRows(output);
|
||||
active = new Map(rows.map((row) => [row.sessionName, row.pid]));
|
||||
// A discovered session's directory, from its first pane with a usable path.
|
||||
// The path is admitted only if it passes the same check createSession and
|
||||
// respawnPane apply, since it was set by whoever owns the pane, not by us.
|
||||
// The launch directory counts as no answer: every pane starts there, and a
|
||||
// remote or docker pane never cds out of it, so for such a session it is
|
||||
// not the workspace — and a rediscovered record cannot tell it is one.
|
||||
for (const row of rows) {
|
||||
if (row.currentPath && !paneDirs.has(row.sessionName) && isValidPath(row.currentPath)) {
|
||||
if (
|
||||
row.currentPath &&
|
||||
!TMUX_LAUNCH_CWD_SPELLINGS.has(row.currentPath) &&
|
||||
!paneDirs.has(row.sessionName) &&
|
||||
isValidPath(row.currentPath)
|
||||
) {
|
||||
paneDirs.set(row.sessionName, row.currentPath);
|
||||
}
|
||||
}
|
||||
@@ -3197,7 +3223,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
mode: 'claude',
|
||||
attached: false,
|
||||
name: `Restored: ${sessionName}`,
|
||||
// Every field above except the name and the pid is a guess: this record
|
||||
// Every field above except the name, the pid and, when tmux reported a
|
||||
// usable one, the directory is a guess: this record
|
||||
// was rebuilt from the socket because Codeman's own bookkeeping did not
|
||||
// have it. The synthetic id also cannot find the session's state.json
|
||||
// entry, so a remote or docker session rediscovered this way arrives
|
||||
|
||||
Reference in new issue
Block a user