diff --git a/.syncwheel/manifest.json b/.syncwheel/manifest.json index b17faf8..9fb9f3e 100644 --- a/.syncwheel/manifest.json +++ b/.syncwheel/manifest.json @@ -10,9 +10,26 @@ "branch": "main-integration", "base": "origin/main", "strategy": "cherry-pick", - "stacks": [] + "stacks": [ + "codex-live-socket" + ] }, - "stacks": [], + "stacks": [ + { + "id": "codex-live-socket", + "branch": "codex/tmux-bridge-live-socket", + "base": "origin/main", + "target_remote": "origin", + "target_branch": "main", + "integration_branch": "main-integration", + "commits": [ + "f22c1d82da4ea4520c9957b00b9ecad1b4dbfe98" + ], + "meta": { + "purpose": "Fix Codex remote socket live detection" + } + } + ], "syncwheel_worktree_root": "var/syncwheel", "syncwheel_tracking": "git-tracked" } diff --git a/packages/tmux-bridge/README.md b/packages/tmux-bridge/README.md index e749fbc..330da44 100644 --- a/packages/tmux-bridge/README.md +++ b/packages/tmux-bridge/README.md @@ -37,6 +37,9 @@ MESH_AGENT_CAPABILITIES="code,review,plan" ## Usage +Prerequisites: `bash`, `tmux`, `python3`, `ss`, and the target agent CLI +(`codex`, `claude`, or another configured agent). + ```bash BIN="packages/tmux-bridge/bin" # relative to repo root @@ -88,12 +91,18 @@ has not changed for the stall window, they return `stalled` / `STALLED` with exi `124`. The caller LLM should inspect status and decide whether to keep waiting, update the user, or ask before stopping the other agent. +For large prompts, `agent-send.sh` waits for bracketed-paste rendering to settle +before submitting, then confirms the submit with a real working/done marker +instead of treating any pane repaint as success. + ## Codex Desktop visibility (remote mode) Bridge Codex sessions show up **inside Codex Desktop automatically** whenever the -desktop app-server socket is live — no env var to remember. `codex.conf` -auto-detects `~/.codex/app-server-control/desktop-ssh-websocket-v0.sock` and adds -`--remote unix://…` when it exists. Overrides: +desktop app-server has a listening socket — no env var to remember. `codex.conf` +checks the known app-server socket candidates in order and adds `--remote +unix://…` only for the first candidate that is actually listening. If no +candidate is live, the bridge falls back to a standalone tmux-only Codex session +instead of failing on a stale socket file. Overrides: | Want | Do | |---|---| diff --git a/packages/tmux-bridge/agents/codex.conf b/packages/tmux-bridge/agents/codex.conf index 45dbf5a..3757065 100644 --- a/packages/tmux-bridge/agents/codex.conf +++ b/packages/tmux-bridge/agents/codex.conf @@ -17,17 +17,27 @@ AGENT_IDLE_PATTERN="gpt-|o[0-9] |claude-" # Regex: explicit "turn finished" marker (used by agent-wait.sh for robust completion detection) AGENT_DONE_PATTERN="Worked for" -# Codex Desktop visibility: when the desktop app-server socket is live, connect the -# TUI to it via --remote so bridge sessions also appear inside Codex Desktop. This is -# now AUTOMATIC — no need to remember to export CODEX_REMOTE_SOCK. Precedence: +# Codex Desktop visibility: when an app-server socket is live, connect the TUI to it +# via --remote so bridge sessions also appear inside Codex Desktop. This is now +# AUTOMATIC — no need to remember to export CODEX_REMOTE_SOCK. Precedence: # * CODEX_REMOTE_SOCK already set (even to "") → respected verbatim (your override) # * CODEX_NO_REMOTE=1 → force standalone, tmux-only session -# * otherwise → auto-detect the desktop socket +# * otherwise → auto-detect the first LISTENING socket +# We must test that the socket is actually listening, not merely present: a stale +# socket file (e.g. a leftover SSH-tunnel websocket) makes `codex --remote` fail with +# "failed to connect to remote app server". Candidates are tried in order; if none is +# live we fall back to a standalone tmux-only session instead of failing the launch. # Verified: --remote coexists with -c overrides (e.g. model_reasoning_effort=xhigh). if [[ -z "${CODEX_REMOTE_SOCK+x}" && -z "${CODEX_NO_REMOTE:-}" ]]; then - _codex_desktop_sock="${HOME}/.codex/app-server-control/desktop-ssh-websocket-v0.sock" - [[ -S "$_codex_desktop_sock" ]] && CODEX_REMOTE_SOCK="$_codex_desktop_sock" - unset _codex_desktop_sock + for _codex_sock in \ + "${HOME}/.codex/app-server-control/desktop-ssh-websocket-v0.sock" \ + "${HOME}/.codex/app-server-control/app-server-control.sock"; do + if [[ -S "$_codex_sock" ]] && ss -lx 2>/dev/null | grep -qF "$_codex_sock"; then + CODEX_REMOTE_SOCK="$_codex_sock" + break + fi + done + unset _codex_sock fi # Resume an existing on-disk session ({SESSION_ID} is substituted by agent-session.sh). diff --git a/packages/tmux-bridge/bin/agent-send.sh b/packages/tmux-bridge/bin/agent-send.sh index bd7a9c3..18d384b 100755 --- a/packages/tmux-bridge/bin/agent-send.sh +++ b/packages/tmux-bridge/bin/agent-send.sh @@ -60,18 +60,30 @@ mtmux delete-buffer -b "$BUFFER_NAME" 2>/dev/null || true trap - EXIT sleep 0.2 -# Submit and CONFIRM submission. A large paste is ingested asynchronously by the -# TUI (shown as a collapsed "[Pasted Content N chars]" placeholder); a submit key -# sent during ingestion is dropped, leaving the prompt unsent. So we send the -# submit key, then verify the turn actually started (working spinner appears or -# the pane changes); if not, re-send the submit key a few times. -_pre_submit="$(mtmux capture-pane -t "$TARGET" -p 2>/dev/null)" +# A large paste lands in several bracketed-paste chunks, each rendered +# asynchronously as a collapsed "[Pasted Content N chars]" placeholder. Wait for +# the pane to stop mutating before submitting, otherwise the still-rendering +# paste looks like "the turn started". +_settle_prev="" +for _ in 1 2 3 4 5 6 7 8; do + sleep 0.5 + _settle_now="$(mtmux capture-pane -t "$TARGET" -p 2>/dev/null)" + [[ "$_settle_now" == "$_settle_prev" ]] && break + _settle_prev="$_settle_now" +done + +# Submit and CONFIRM the turn actually started. Confirmation keys on the working +# spinner ONLY — NOT "the pane changed" (the paste keeps repainting the pane as it +# renders) and NOT the done marker (the PREVIOUS turn's "Worked for …" lingers on +# screen and would falsely confirm after a single dropped submit key). A new turn +# always shows the working spinner, which a finished turn does not. Keep re-sending +# until it appears; submit keys hitting an already-empty composer are harmless. submitted=0 -for _attempt in 1 2 3 4; do +for _attempt in 1 2 3 4 5 6 7 8; do mtmux send-keys -t "$TARGET" "$AGENT_SUBMIT_KEY" sleep 1 _now="$(mtmux capture-pane -t "$TARGET" -p 2>/dev/null)" - if echo "$_now" | grep -qE "$AGENT_WORKING_PATTERN" || [[ "$_now" != "$_pre_submit" ]]; then + if echo "$_now" | grep -qE "$AGENT_WORKING_PATTERN"; then submitted=1; break fi done diff --git a/skills/agent-tmux/SKILL.md b/skills/agent-tmux/SKILL.md index 7dbdb83..b3ed2cf 100644 --- a/skills/agent-tmux/SKILL.md +++ b/skills/agent-tmux/SKILL.md @@ -34,7 +34,7 @@ export AGENT_MESH_ROOT="" BIN="${AGENT_MESH_ROOT}/packages/tmux-bridge/bin" ``` -Prerequisites: `bash`, `tmux`, `python3`, and the target agent CLI (`codex` +Prerequisites: `bash`, `tmux`, `python3`, `ss`, and the target agent CLI (`codex` and/or `claude`). All sessions share the dedicated tmux socket `mesh` (`tmux -L mesh`), kept alive with `exit-empty off` — never the default server. @@ -106,9 +106,11 @@ Codex bridge sessions attach to the desktop **app-server automatically** wheneve its socket is live — they show up inside Codex Desktop and are co-pilotable from mobile over the remote-control tunnel. No env var to remember. -- `codex.conf` auto-detects `~/.codex/app-server-control/desktop-ssh-websocket-v0.sock` - and adds `--remote unix://…`; it also passes `--cd ""` (in remote mode the - app-server ignores `tmux -c`, so this keeps the session in the right project). +- `codex.conf` checks the known app-server socket candidates in order and adds + `--remote unix://…` only for the first candidate that is actually listening; if + none is live, it falls back to a standalone tmux-only session. It also passes + `--cd ""` in remote mode (the app-server ignores `tmux -c`, so this keeps + the session in the right project). - Override: `CODEX_REMOTE_SOCK=/path.sock` forces a socket; `CODEX_NO_REMOTE=1` forces a standalone, tmux-only session (use for long unattended runs you want isolated from daemon restarts). @@ -169,6 +171,9 @@ See `references/agent-mesh-repo-sync.md` for the full sequence and pitfalls. `tmux send-keys … Enter` directly for prompt text. - **Multiline / special chars.** `agent-send.sh` injects prompts via `tmux paste-buffer`; always use it, never raw `send-keys`. +- **Large prompts.** `agent-send.sh` waits for bracketed-paste rendering to + settle before submitting, then confirms submit via working/done markers so a + paste repaint is not mistaken for a started turn. - **Codex trust dialog.** New directories prompt a workspace-trust dialog; `agent-session.sh` auto-confirms with Enter. If stuck, check `--status` and capture the pane with `--full`.