From 2804e5f52f2f28ecd1e9c90f23e3c2bd3091009d Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 26 May 2026 12:19:57 +0000 Subject: [PATCH] fix the sendkeys function and hit enter on "Stop and wait for limit to reset"-dialog --- src/monitor.js | 10 +++++++++- src/patterns.js | 13 ++++++++++++- src/tmux.js | 19 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/monitor.js b/src/monitor.js index 3ee6cf1..02a3958 100644 --- a/src/monitor.js +++ b/src/monitor.js @@ -1,4 +1,4 @@ -import { stripAnsi, isRateLimited, findRateLimitMessage } from './patterns.js'; +import { stripAnsi, isRateLimited, findRateLimitMessage, isLimitPrompt } from './patterns.js'; import { parseResetTime, calculateWaitMs } from './time-parser.js'; import { capturePane, sendKeys, getPaneCommand, isProcessForeground } from './tmux.js'; import { loadConfig } from './config.js'; @@ -67,6 +67,13 @@ export async function processOneTick(state, tmuxAdapter, pane, config, isAlive) return 'waiting'; } + // Detect the interactive "What do you want to do?" prompt and auto-select "Stop and wait" + if (isLimitPrompt(stripped)) { + state.waitUntil = Date.now() + 30_000; + await tmuxAdapter.sendKeys(pane, ''); // Empty string = just press Enter + return 'prompt-confirmed'; + } + return 'monitoring'; } @@ -95,6 +102,7 @@ export async function startMonitor(pane, pid) { } if (result === 'retried') await logger.info(`Sent retry message (attempt ${state.attempts})`); if (result === 'user-continued') await logger.info('User already continued. Attempt counter reset.'); + if (result === 'prompt-confirmed') await logger.info('Limit prompt detected. Sent Enter to select "Stop and wait for limit to reset".'); if (result === 'max-retries') await logger.warn(`Max retries (${config.maxRetries}) reached. Monitor still active but will not send further retries until rate limit clears.`); if (result === 'skipped-not-claude') await logger.warn(`Foreground is "${state._lastForeground}", not Claude. Skipping send-keys. (Add to foregroundCommands in ~/.claude-auto-retry.json if this is wrong)`); } catch (err) { diff --git a/src/patterns.js b/src/patterns.js index 3a6cffa..60d2644 100644 --- a/src/patterns.js +++ b/src/patterns.js @@ -23,7 +23,7 @@ export function stripAnsi(text) { // Detection: find a "limit" line and a "resets" line within 6 lines of each other. const LIMIT_PATTERNS = [ - /(?:hit|exceeded|reached).*?limit/i // matches: "hit ... limit" with anything in between + /(?:hit|exceeded|reached).*?limit/i, // "hit/exceeded/reached ... limit" (handles "You've hit your session limit") /\d+-hour limit/i, // "5-hour limit" /limit reached/i, // "limit reached" /usage limit/i, // "usage limit" @@ -85,3 +85,14 @@ export function findRateLimitMessage(text, customPatterns = []) { return null; } + +// Detects the interactive Claude Code prompt asking user to wait or upgrade +// "What do you want to do?" +// "❯ 1. Stop and wait for limit to reset" +// " 2. Upgrade your plan" +// We auto-select option 1 by sending Enter +export function isLimitPrompt(text) { + const stripped = stripAnsi(text); + return /What do you want to do\?/i.test(stripped) && + /Stop and wait.*limit/i.test(stripped); +} diff --git a/src/tmux.js b/src/tmux.js index 7fc0fc2..2693d7a 100644 --- a/src/tmux.js +++ b/src/tmux.js @@ -8,7 +8,13 @@ export function buildCaptureArgs(pane, lines = 200) { } export function buildSendKeysArgs(pane, text) { - return ['send-keys', '-t', pane, text, 'Enter']; + // Use -l flag to send text literally, then C-m to send (carriage return/Enter) + // This ensures special characters and spaces in the text are handled correctly + if (text) { + return [['send-keys', '-t', pane, '-l', text], ['send-keys', '-t', pane, 'C-m']]; + } + // For empty string, just send C-m + return [['send-keys', '-t', pane, 'C-m']]; } export function buildDisplayArgs(pane, format) { @@ -32,7 +38,16 @@ export async function capturePane(pane, lines = 200) { } export async function sendKeys(pane, text) { - await execFileAsync('tmux', buildSendKeysArgs(pane, text)); + const args = buildSendKeysArgs(pane, text); + // If buildSendKeysArgs returns nested arrays (multiple commands), execute them sequentially + if (Array.isArray(args[0])) { + for (const cmdArgs of args) { + await execFileAsync('tmux', cmdArgs); + } + } else { + // Single command + await execFileAsync('tmux', args); + } } export async function getPaneCommand(pane) {