Merge PR #12: fix 3 issues (sendkeys, session limit regex, stop-and-wait dialog)

This commit is contained in:
2026-06-01 00:13:00 +02:00
3 changed files with 38 additions and 4 deletions
+9 -1
View File
@@ -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 { parseResetTime, calculateWaitMs } from './time-parser.js';
import { capturePane, sendKeys, getPaneCommand, isProcessForeground } from './tmux.js'; import { capturePane, sendKeys, getPaneCommand, isProcessForeground } from './tmux.js';
import { loadConfig } from './config.js'; import { loadConfig } from './config.js';
@@ -67,6 +67,13 @@ export async function processOneTick(state, tmuxAdapter, pane, config, isAlive)
return 'waiting'; 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'; 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 === '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 === '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 === '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)`); 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) { } catch (err) {
+12 -1
View File
@@ -23,7 +23,7 @@ export function stripAnsi(text) {
// Detection: find a "limit" line and a "resets" line within 6 lines of each other. // Detection: find a "limit" line and a "resets" line within 6 lines of each other.
const LIMIT_PATTERNS = [ const LIMIT_PATTERNS = [
/(?:hit|exceeded|reached).*(?:your|the)\s*(?:\d+-hour\s+)?limit/i, // "hit/exceeded/reached your limit" /(?:hit|exceeded|reached).*?limit/i, // "hit/exceeded/reached ... limit" (handles "You've hit your session limit")
/\d+-hour limit/i, // "5-hour limit" /\d+-hour limit/i, // "5-hour limit"
/limit reached/i, // "limit reached" /limit reached/i, // "limit reached"
/usage limit/i, // "usage limit" /usage limit/i, // "usage limit"
@@ -85,3 +85,14 @@ export function findRateLimitMessage(text, customPatterns = []) {
return null; 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);
}
+17 -2
View File
@@ -8,7 +8,13 @@ export function buildCaptureArgs(pane, lines = 200) {
} }
export function buildSendKeysArgs(pane, text) { 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) { export function buildDisplayArgs(pane, format) {
@@ -32,7 +38,16 @@ export async function capturePane(pane, lines = 200) {
} }
export async function sendKeys(pane, text) { 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) { export async function getPaneCommand(pane) {