From 515c528e10a868bec457be63816b88f85b405568 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 17 Mar 2026 22:21:46 +0000 Subject: [PATCH] fix: poll for claude readiness before injecting prompt into tmux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace fixed sleep(3) + paste-buffer race with a wait_for_claude_ready() function that polls the tmux pane for the ❯ prompt (up to 120s). This fixes the bug where the initial prompt was pasted before Claude Code finished initializing, resulting in a stuck session with an empty prompt. Observed on issue #81: session sat idle for 42+ minutes because the paste arrived during Claude's startup splash screen. Changes: - Add wait_for_claude_ready() that polls tmux capture-pane for ❯ - Call it inside inject_into_session() before every paste - Use inject_into_session() for initial prompt (was inline paste-buffer) - Remove fixed sleep(3) from session creation and recovery paths - Fail hard if claude doesn't become ready within timeout Co-Authored-By: Claude Opus 4.6 --- dev/dev-agent.sh | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index b22264b..55de5d0 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -79,9 +79,25 @@ read_phase() { { cat "$PHASE_FILE" 2>/dev/null || true; } | head -1 | tr -d '[:space:]' } +wait_for_claude_ready() { + local timeout="${1:-120}" + local elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + # Claude Code shows ❯ when ready for input + if tmux capture-pane -t "${SESSION_NAME}" -p 2>/dev/null | grep -q '❯'; then + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + log "WARNING: claude not ready after ${timeout}s — proceeding anyway" + return 1 +} + inject_into_session() { local text="$1" local tmpfile + wait_for_claude_ready 120 tmpfile=$(mktemp /tmp/tmux-inject-XXXXXX) printf '%s' "$text" > "$tmpfile" tmux load-buffer -b "inject-${ISSUE}" "$tmpfile" @@ -811,9 +827,6 @@ if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then tmux new-session -d -s "${SESSION_NAME}" -c "${WORKTREE}" \ "claude --dangerously-skip-permissions" - # Wait for Claude to initialize - sleep 3 - if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then log "ERROR: failed to create tmux session ${SESSION_NAME}" cleanup_labels @@ -821,20 +834,21 @@ if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then exit 1 fi log "tmux session created: ${SESSION_NAME}" + + # Wait for Claude to be ready (polls for ❯ prompt) + if ! wait_for_claude_ready 120; then + log "ERROR: claude did not become ready in ${SESSION_NAME}" + kill_tmux_session + cleanup_labels + cleanup_worktree + exit 1 + fi else log "reusing existing tmux session: ${SESSION_NAME}" fi -# Send initial prompt via paste buffer (handles long text and special chars) -PROMPT_TMPFILE=$(mktemp /tmp/dev-prompt-XXXXXX) -printf '%s' "$INITIAL_PROMPT" > "$PROMPT_TMPFILE" -tmux load-buffer -b "prompt-${ISSUE}" "$PROMPT_TMPFILE" -tmux paste-buffer -t "${SESSION_NAME}" -b "prompt-${ISSUE}" -sleep 1 -tmux send-keys -t "${SESSION_NAME}" "" Enter -tmux delete-buffer -b "prompt-${ISSUE}" 2>/dev/null || true -rm -f "$PROMPT_TMPFILE" - +# Send initial prompt (inject_into_session waits for claude to be ready) +inject_into_session "$INITIAL_PROMPT" log "initial prompt sent to tmux session" # Signal to dev-poll.sh that we're running (session is up) @@ -882,7 +896,6 @@ Phase file: ${PHASE_FILE}" if tmux new-session -d -s "${SESSION_NAME}" -c "${WORKTREE}" \ "claude --dangerously-skip-permissions" 2>/dev/null; then - sleep 3 inject_into_session "$RECOVERY_MSG" log "recovery session started" IDLE_ELAPSED=0