把目标 Codex TUI pane 重启到当前账号,并尽量 resume 回原来的 Codex session。能精确恢复 session 时必须恢复;不能精确恢复时,不要用 --last 猜。
-
固定 tmux 入口,后续命令都用同一个变量。不要在同一次操作里混用系统 tmux 和用户 wrapper;如果 $HOME/bin/tmux 存在,优先用它,因为它可能封装了正确 socket / 环境:
TMUX_BIN="$(command -v tmux || true)"
if [[ -x "$HOME/bin/tmux" ]]; then
TMUX_BIN="$HOME/bin/tmux"
fi
[[ -n "$TMUX_BIN" ]] || { echo "tmux not found" >&2; exit 1; }
-
先确认当前 pane,批量操作时跳过它,避免中断本次执行:
"$TMUX_BIN" display-message -p '#S:#I.#{pane_index} #{pane_id}'
-
列出候选 pane,保留 window_id 和 window_panes。默认只处理 pane_current_command=node 且子进程是 Codex CLI 的 pane;不要碰普通 shell、vim、ssh、训练任务或用户正在操作的非 Codex pane:
"$TMUX_BIN" list-panes -a -F '#{session_name}:#{window_index}.#{pane_index}\t#{window_id}\t#{pane_id}\t#{window_name}\t#{pane_current_command}\t#{pane_current_path}\t#{pane_pid}\t#{window_panes}'
-
对每个 Codex pane,用进程树找到 Rust 侧 Codex 进程,再用 lsof 读取它打开的 session jsonl。这个比从 pane 文本或目录猜 session 更可靠:
pane_pid=<pane_pid>
codex_node_pattern="${CODEX_NODE_PATTERN:-$HOME/apps/nodejs/bin/codex|.*/@openai/codex/bin/codex}"
node_pid="$(pgrep -P "$pane_pid" -f "$codex_node_pattern" | head -n1)"
rust_pid="$(pgrep -P "$node_pid" -f '/codex-linux-.*/bin/codex|/bin/codex' | head -n1)"
session_file="$(lsof -p "$rust_pid" 2>/dev/null | awk '{print $9}' | grep -E '/\.codex/sessions/.+\.jsonl$' | head -n1)"
session_id="$(basename "$session_file" .jsonl | sed -E 's/^rollout-[0-9TZ:-]+-//')"
-
构造启动命令时用 bash array 和 printf %q,避免手写多层引号。需要刷新环境变量时,把它们显式放进 env,并在后续验证 /proc/<rust_pid>/environ:
extra_env=()
[[ -n "${RUST_LOG:-}" ]] && extra_env+=("RUST_LOG=$RUST_LOG")
[[ -n "${CODEX_SQLITE_HOME:-}" ]] && extra_env+=("CODEX_SQLITE_HOME=$CODEX_SQLITE_HOME")
inner="codexr resume $session_id; rc=\$?; echo; echo codexr exited with status \$rc; exec zsh -l"
cmd_parts=(env "TMUX_AI_WINDOW_NAME=$window_name" "${extra_env[@]}" zsh -ic "$inner")
printf -v command '%q ' "${cmd_parts[@]}"
-
单 pane window 不要直接 respawn-pane -k。安全流程是:先在同一个 session 创建临时 window,确认新 Codex 进程已经起来并带着目标环境,再 kill 旧 window、move 临时 window 回原 index、恢复原 window 名。这样启动失败时旧窗口还在:
tmp_wid="$("$TMUX_BIN" new-window -d -P -F '#{window_id}' -t "=<session>:" -n "__respawn_<index>_<name>" -c "$pane_current_path" "$command")"
"$TMUX_BIN" kill-window -t "$old_window_id"
"$TMUX_BIN" move-window -s "$tmp_wid" -t "=<session>:<old_index>"
"$TMUX_BIN" rename-window -t "=<session>:<old_index>" "$window_name"
-
多 pane window 只替换目标 pane。先 split-window -d 创建新 pane,确认新 Codex 进程和环境正确,再 swap-pane 到旧 pane 位置并 kill 旧 pane。不要整窗替换多 pane window:
new_pane="$("$TMUX_BIN" split-window -d -t "$old_pane_id" -c "$pane_current_path" -P -F '#{pane_id}' "$command")"
"$TMUX_BIN" swap-pane -s "$new_pane" -t "$old_pane_id"
"$TMUX_BIN" kill-pane -t "$old_pane_id"
-
如果某个 Codex pane 没有打开 session jsonl:
- 先用
/status 或 tmux capture-pane 看它是否已经是新账号。
- 如果能从
/status 得到明确 session id,可以用该 id resume。
- 如果是空会话、0 token、或无法精确得到 session id,可以 fresh respawn;不要用
codex resume --last。
-
respawn 后核对进程和关键 pane:
"$TMUX_BIN" display-message -p -t "=<session>:<window>.<pane>" '#{pane_pid} #{window_name} #{pane_current_command} #{pane_current_path}'
"$TMUX_BIN" capture-pane -t "=<session>:<window>.<pane>" -p -S -80 | tail -n 40
tr '\0' '\n' < /proc/<rust_pid>/environ | grep -E '^(RUST_LOG|CODEX_SQLITE_HOME|CODEX_HOME)='