Skip to content

Commit 15123cf

Browse files
rysweetclaude
andauthored
fix(auto-mode): improve timeout handling for Opus model (#1667) (#1676)
* fix(auto-mode): improve timeout handling for Opus model (#1667) - Increase default timeout from 5 min to 30 min - Add --no-timeout flag for unlimited execution - Auto-detect Opus model and use 60 min timeout - Add comprehensive tests for timeout resolution Fixes timeout issues where Opus appeared to skip workflow steps but was actually being terminated mid-execution. Closes #1667 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add timeout configuration documentation and discovery - Added 'Per-Turn Timeout' section to AUTO_MODE.md with examples - Added 'Turn Timeouts' troubleshooting entry - Documented the Opus timeout discovery in DISCOVERIES.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad25cf8 commit 15123cf

File tree

5 files changed

+610
-16
lines changed

5 files changed

+610
-16
lines changed

.claude/context/DISCOVERIES.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@ What insights should be remembered?
5050

5151
---
5252

53+
## Auto Mode Timeout Causing Opus Model Workflow Failures (2025-11-26)
54+
55+
### Problem
56+
57+
Opus model was "skipping" workflow steps during auto mode execution. Investigation revealed the 5-minute per-turn timeout was cutting off Opus execution mid-workflow due to extended thinking requirements.
58+
59+
### Root Cause
60+
61+
The default per-turn timeout of 5 minutes was too aggressive for Opus model, which requires extended thinking time. Log analysis showed:
62+
63+
- `Turn 2 timed out after 300.0s`
64+
- `Turn 1 timed out after 600.1s`
65+
66+
### Solution (PR #1676)
67+
68+
Implemented flexible timeout resolution system:
69+
70+
1. **Increased default timeout**: 5 min → 30 min
71+
2. **Added `--no-timeout` flag**: Disables timeout entirely using `nullcontext()`
72+
3. **Opus auto-detection**: Model names containing "opus" automatically get 60 min timeout
73+
4. **Clear priority system**: `--no-timeout` > explicit > auto-detect > default
74+
75+
### Key Insight
76+
77+
**Extended thinking models like Opus need significantly longer timeouts.** Auto-detection based on model name provides a good default without requiring users to remember to adjust settings.
78+
79+
### Files Changed
80+
81+
- `src/amplihack/cli.py`: Added `--no-timeout` flag and `resolve_timeout()` function
82+
- `src/amplihack/launcher/auto_mode.py`: Accept `None` timeout using `nullcontext`
83+
- `tests/unit/test_auto_mode_timeout.py`: 19 comprehensive tests
84+
- `docs/AUTO_MODE.md`: Added timeout configuration documentation
85+
5386
## Power-Steering Session Type Detection Fix (2025-11-25)
5487

5588
### Problem

docs/AUTO_MODE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,35 @@ Adjust based on task complexity:
149149
amplihack claude --auto --max-turns 25 -- -p "complex multi-module refactoring"
150150
```
151151

152+
### Per-Turn Timeout
153+
154+
Default: 30 minutes per turn
155+
156+
Controls how long each turn can run before timing out. This prevents runaway executions while allowing complex operations to complete.
157+
158+
**Priority order (highest to lowest):**
159+
160+
1. `--no-timeout` flag (disables timeout entirely)
161+
2. Explicit `--query-timeout-minutes` value
162+
3. Auto-detection (Opus models → 60 minutes)
163+
4. Default (30 minutes)
164+
165+
```bash
166+
# Use default 30-minute timeout
167+
amplihack claude --auto -- -p "implement feature"
168+
169+
# Explicit timeout (45 minutes)
170+
amplihack claude --auto --query-timeout-minutes 45 -- -p "complex refactoring"
171+
172+
# Disable timeout for very long operations
173+
amplihack claude --auto --no-timeout -- -p "comprehensive codebase analysis"
174+
175+
# Opus model auto-detects to 60 minutes
176+
amplihack claude --auto -- --model opus -p "architectural design"
177+
```
178+
179+
**Note:** Opus models automatically use 60-minute timeouts due to extended thinking requirements. Use `--no-timeout` for operations expected to exceed 60 minutes.
180+
152181
### Session Logging
153182

154183
All auto mode sessions are logged to:
@@ -399,6 +428,17 @@ Auto mode excels at:
399428
**Cause**: Syntax errors, test failures during execution
400429
**Solution**: Auto mode logs errors and continues. Review logs in `.claude/runtime/logs/` to see what happened.
401430

431+
### Turn Timeouts
432+
433+
**Cause**: A turn exceeded the per-turn timeout (default 30 minutes)
434+
**Solution**:
435+
436+
- Check logs for `Turn N timed out after X seconds`
437+
- For Opus models, ensure auto-detection is working (uses 60 min automatically)
438+
- Use `--query-timeout-minutes 60` for longer operations
439+
- Use `--no-timeout` for very long operations (use with caution)
440+
- Consider breaking complex tasks into smaller subtasks
441+
402442
### Installation Issues (Copilot)
403443

404444
**Cause**: GitHub Copilot CLI not installed

src/amplihack/cli.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,18 @@ def handle_auto_mode(
244244
# Check if UI mode is enabled
245245
ui_mode = getattr(args, "ui", False)
246246

247-
# Extract timeout from args
248-
query_timeout = getattr(args, "query_timeout_minutes", 5.0)
247+
# Extract model from cmd_args for timeout auto-detection
248+
model = None
249+
if cmd_args and "--model" in cmd_args:
250+
try:
251+
model_idx = cmd_args.index("--model")
252+
if model_idx + 1 < len(cmd_args):
253+
model = cmd_args[model_idx + 1]
254+
except (ValueError, IndexError):
255+
pass
256+
257+
# Resolve timeout using priority: --no-timeout > explicit > model auto-detect > default
258+
query_timeout = resolve_timeout(args, model)
249259

250260
auto = AutoMode(
251261
sdk, prompt, args.max_turns, ui_mode=ui_mode, query_timeout_minutes=query_timeout
@@ -358,14 +368,51 @@ def add_auto_mode_args(parser: argparse.ArgumentParser) -> None:
358368
parser.add_argument(
359369
"--query-timeout-minutes",
360370
type=float,
361-
default=5.0,
371+
default=30.0,
362372
metavar="MINUTES",
363373
help=(
364-
"Timeout for each SDK query in minutes (default: 5.0). "
365-
"Prevents indefinite hangs in complex sessions. "
366-
"Use higher values (10-15) for very long-running operations."
374+
"Timeout for each SDK query in minutes (default: 30). "
375+
"Opus models auto-detect to 60 minutes. "
376+
"Use --no-timeout to disable timeout completely."
367377
),
368378
)
379+
parser.add_argument(
380+
"--no-timeout",
381+
action="store_true",
382+
help="Disable timeout for SDK queries (allows indefinite execution).",
383+
)
384+
385+
386+
def resolve_timeout(args: argparse.Namespace, model: Optional[str] = None) -> Optional[float]:
387+
"""Resolve timeout value based on CLI args and model detection.
388+
389+
Priority order:
390+
1. --no-timeout flag (returns None)
391+
2. Explicit --query-timeout-minutes value (if not default 30.0)
392+
3. Auto-detect Opus model (60 minutes)
393+
4. Default from argparse (30 minutes)
394+
395+
Args:
396+
args: Parsed command line arguments
397+
model: Model name from --model arg (for Opus detection)
398+
399+
Returns:
400+
Timeout in minutes, or None for no timeout
401+
"""
402+
# Priority 1: --no-timeout flag takes precedence
403+
if getattr(args, "no_timeout", False):
404+
return None
405+
406+
# Get the timeout value (defaults to 30.0 from argparse)
407+
timeout = getattr(args, "query_timeout_minutes", 30.0)
408+
409+
# Priority 3: Auto-detect Opus model (60 minute timeout)
410+
# Only apply if timeout is the default (30.0), meaning user didn't explicitly override
411+
if model and "opus" in model.lower() and timeout == 30.0:
412+
return 60.0
413+
414+
# Return the timeout value (explicit or default)
415+
return timeout
369416

370417

371418
def add_common_sdk_args(parser: argparse.ArgumentParser) -> None:

src/amplihack/launcher/auto_mode.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import threading
1111
import time
12+
from contextlib import nullcontext
1213
from pathlib import Path
1314
from typing import Optional, Tuple
1415

@@ -80,7 +81,7 @@ def __init__(
8081
max_turns: int = 10,
8182
working_dir: Optional[Path] = None,
8283
ui_mode: bool = False,
83-
query_timeout_minutes: float = 5.0,
84+
query_timeout_minutes: Optional[float] = 30.0,
8485
):
8586
"""Initialize auto mode.
8687
@@ -90,7 +91,8 @@ def __init__(
9091
max_turns: Max iterations (default 10)
9192
working_dir: Working directory (defaults to current dir)
9293
ui_mode: Enable interactive UI mode (requires Rich library)
93-
query_timeout_minutes: Timeout for each SDK query in minutes (default 5.0)
94+
query_timeout_minutes: Timeout for each SDK query in minutes (default 30.0).
95+
None disables timeout. Opus detection is handled by cli.py:resolve_timeout().
9496
"""
9597
self.sdk = sdk
9698
self.prompt = prompt
@@ -100,13 +102,23 @@ def __init__(
100102
self.working_dir = working_dir if working_dir is not None else Path.cwd()
101103
self.ui_enabled = ui_mode
102104
self.ui = None
103-
# Validate timeout value
104-
if query_timeout_minutes <= 0:
105-
raise ValueError(f"query_timeout_minutes must be positive, got {query_timeout_minutes}")
106-
if query_timeout_minutes > 120: # 2 hours max - warn but allow
107-
print(f"Warning: Very long timeout ({query_timeout_minutes} minutes)", file=sys.stderr)
108105

109-
self.query_timeout_seconds = query_timeout_minutes * 60.0 # Keep as float for precision
106+
# Handle timeout: None means no timeout, otherwise validate
107+
if query_timeout_minutes is None:
108+
# No timeout - used for --no-timeout flag
109+
self.query_timeout_seconds: Optional[float] = None
110+
else:
111+
# Validate positive timeout
112+
if query_timeout_minutes <= 0:
113+
raise ValueError(
114+
f"query_timeout_minutes must be positive, got {query_timeout_minutes}"
115+
)
116+
if query_timeout_minutes > 120: # 2 hours max - warn but allow
117+
print(
118+
f"Warning: Very long timeout ({query_timeout_minutes} minutes)", file=sys.stderr
119+
)
120+
121+
self.query_timeout_seconds = query_timeout_minutes * 60.0 # Keep as float for precision
110122
self.log_dir = (
111123
self.working_dir / ".claude" / "runtime" / "logs" / f"auto_{sdk}_{int(time.time())}"
112124
)
@@ -527,8 +539,13 @@ async def _run_turn_with_sdk(self, prompt: str) -> Tuple[int, str]:
527539
# Track turn start time for accurate timeout reporting
528540
turn_start_time = time.time()
529541

530-
# Add timeout wrapper around SDK query
531-
async with asyncio.timeout(self.query_timeout_seconds):
542+
# Add timeout wrapper around SDK query (use nullcontext if no timeout)
543+
timeout_ctx = (
544+
asyncio.timeout(self.query_timeout_seconds)
545+
if self.query_timeout_seconds is not None
546+
else nullcontext()
547+
)
548+
async with timeout_ctx:
532549
async for message in query(prompt=prompt, options=options):
533550
print("\n[DEBUG] 💬 Got a message from query()", flush=True)
534551
# Handle different message types

0 commit comments

Comments
 (0)