Skip to content

Commit 1f74b88

Browse files
gmccrackinclaude
andcommitted
Add stuck loop detection and failure tracking for features
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 07e5dc0 commit 1f74b88

File tree

6 files changed

+270
-9
lines changed

6 files changed

+270
-9
lines changed

.claude/templates/coding_prompt.template.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,31 @@ Use browser automation tools:
189189
- [ ] Loading states appeared during API calls
190190
- [ ] Error states handle failures gracefully
191191

192-
### STEP 6.6: MOCK DATA DETECTION SWEEP
192+
### STEP 6.6: HANDLING TOOL FAILURES
193+
194+
#### Playwright "Not connected" or Timeout Errors
195+
196+
If browser tools repeatedly fail with "Not connected" or timeout errors:
197+
198+
1. **Do NOT retry more than 3 times** - Repeated failures indicate the MCP server may have disconnected
199+
2. **Record the failure** using `feature_record_failure` tool with the feature ID and error message
200+
3. **Update progress notes** in `claude-progress.txt` documenting the issue
201+
4. **Commit your progress** so work isn't lost
202+
5. **Let the session end** - The system will detect the stuck loop and restart with fresh MCP connections
203+
204+
The session will automatically restart and resume the in-progress feature with working browser tools.
205+
206+
#### General Error Recovery
207+
208+
If ANY tool fails repeatedly (3+ times with the same error):
209+
1. Stop retrying - the issue likely requires a fresh session
210+
2. Call `feature_record_failure` with the feature ID and error message
211+
3. Commit any progress made
212+
4. Let the session end naturally
213+
214+
**Never retry the same failing operation more than 3 times in a row.**
215+
216+
### STEP 6.7: MOCK DATA DETECTION SWEEP (OPTIONAL)
193217

194218
**Run this sweep AFTER EVERY FEATURE before marking it as passing:**
195219

@@ -359,6 +383,9 @@ feature_mark_passing with feature_id={id}
359383
360384
# 5. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
361385
feature_skip with feature_id={id}
386+
387+
# 6. Record a failure (when tools repeatedly fail) - increments failure count
388+
feature_record_failure with feature_id={id} and error_message="description"
362389
```
363390

364391
### RULES:

agent.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
AUTO_CONTINUE_DELAY_SECONDS = 3
2626
STOP_FILE_NAME = ".stop_requested"
2727

28+
# Stuck loop detection
29+
MAX_CONSECUTIVE_SAME_ERRORS = 5
30+
2831

2932
def check_stop_requested(project_dir: Path) -> bool:
3033
"""
@@ -52,6 +55,34 @@ def request_stop(project_dir: Path) -> None:
5255
print("The agent will stop after completing the current feature.")
5356

5457

58+
def _normalize_error_for_comparison(error_str: str) -> str:
59+
"""
60+
Normalize error string for stuck loop detection.
61+
62+
Extracts the key part of the error message to detect repeated patterns
63+
even if details vary slightly.
64+
"""
65+
# Truncate to first 100 chars for comparison
66+
normalized = error_str[:100].strip().lower()
67+
return normalized
68+
69+
70+
def _check_stuck_loop(error_history: list[str]) -> bool:
71+
"""
72+
Check if the error history indicates a stuck loop.
73+
74+
Returns True if the last N errors are all identical (same normalized message).
75+
"""
76+
if len(error_history) < MAX_CONSECUTIVE_SAME_ERRORS:
77+
return False
78+
79+
recent = error_history[-MAX_CONSECUTIVE_SAME_ERRORS:]
80+
normalized = [_normalize_error_for_comparison(e) for e in recent]
81+
82+
# All recent errors are the same
83+
return len(set(normalized)) == 1
84+
85+
5586
async def run_agent_session(
5687
client: ClaudeSDKClient,
5788
message: str,
@@ -69,9 +100,13 @@ async def run_agent_session(
69100
(status, response_text) where status is:
70101
- "continue" if agent should continue working
71102
- "error" if an error occurred
103+
- "stuck" if stuck loop detected (repeated identical errors)
72104
"""
73105
print("Sending prompt to Claude Agent SDK...\n")
74106

107+
# Track consecutive errors for stuck loop detection
108+
error_history: list[str] = []
109+
75110
try:
76111
# Send the query
77112
await client.query(message)
@@ -114,8 +149,24 @@ async def run_agent_session(
114149
# Show errors (truncated)
115150
error_str = str(result_content)[:500]
116151
print(f" [Error] {error_str}", flush=True)
152+
153+
# Track error for stuck loop detection
154+
error_history.append(error_str)
155+
156+
# Check for stuck loop
157+
if _check_stuck_loop(error_history):
158+
print("\n" + "=" * 70)
159+
print(" STUCK LOOP DETECTED")
160+
print("=" * 70)
161+
print(f"\nSame error repeated {MAX_CONSECUTIVE_SAME_ERRORS} times:")
162+
print(f" {error_history[-1][:100]}...")
163+
print("\nEnding session for clean restart.")
164+
print("The MCP servers will reconnect in the next session.")
165+
print("-" * 70 + "\n")
166+
return "stuck", f"Stuck loop detected: {error_history[-1][:200]}"
117167
else:
118-
# Tool succeeded - just show brief confirmation
168+
# Tool succeeded - reset error history
169+
error_history.clear()
119170
print(" [Done]", flush=True)
120171

121172
print("\n" + "-" * 70 + "\n")
@@ -218,6 +269,12 @@ async def run_autonomous_agent(
218269
print_progress_summary(project_dir)
219270
await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)
220271

272+
elif status == "stuck":
273+
print("\nSession ended due to stuck loop detection")
274+
print("Starting fresh session with reconnected MCP servers...")
275+
print_progress_summary(project_dir)
276+
await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)
277+
221278
elif status == "error":
222279
print("\nSession encountered an error")
223280
print("Will retry with a fresh session...")

api/database.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class Feature(Base):
2929
steps = Column(JSON, nullable=False) # Stored as JSON array
3030
passes = Column(Boolean, default=False, index=True)
3131
in_progress = Column(Boolean, default=False, index=True)
32+
# Failure tracking for stuck loop detection
33+
failure_count = Column(Integer, default=0)
34+
last_error = Column(Text, nullable=True)
3235

3336
def to_dict(self) -> dict:
3437
"""Convert feature to dictionary for JSON serialization."""
@@ -41,6 +44,8 @@ def to_dict(self) -> dict:
4144
"steps": self.steps,
4245
"passes": self.passes,
4346
"in_progress": self.in_progress,
47+
"failure_count": self.failure_count or 0,
48+
"last_error": self.last_error,
4449
}
4550

4651

api/migration.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,60 @@ def migrate_add_in_progress_column(
198198
return False
199199
finally:
200200
session.close()
201+
202+
203+
def migrate_add_failure_tracking_columns(
204+
project_dir: Path,
205+
session_maker: sessionmaker,
206+
) -> bool:
207+
"""
208+
Add failure_count and last_error columns to existing databases.
209+
210+
This migration adds columns for tracking consecutive failures on features:
211+
- failure_count: Number of consecutive session failures
212+
- last_error: Last error message encountered
213+
214+
These enable stuck loop detection and auto-skipping of problematic features.
215+
216+
Args:
217+
project_dir: Directory containing the project
218+
session_maker: SQLAlchemy session maker
219+
220+
Returns:
221+
True if migration was performed, False if columns already exist
222+
"""
223+
session: Session = session_maker()
224+
try:
225+
# Check existing columns using PRAGMA
226+
result = session.execute(text("PRAGMA table_info(features)"))
227+
columns = [row[1] for row in result.fetchall()]
228+
229+
added_any = False
230+
231+
# Add failure_count column if missing
232+
if "failure_count" not in columns:
233+
session.execute(
234+
text("ALTER TABLE features ADD COLUMN failure_count INTEGER DEFAULT 0")
235+
)
236+
print("Added failure_count column to features table")
237+
added_any = True
238+
239+
# Add last_error column if missing
240+
if "last_error" not in columns:
241+
session.execute(
242+
text("ALTER TABLE features ADD COLUMN last_error TEXT")
243+
)
244+
print("Added last_error column to features table")
245+
added_any = True
246+
247+
if added_any:
248+
session.commit()
249+
250+
return added_any
251+
252+
except Exception as e:
253+
session.rollback()
254+
print(f"Error adding failure tracking columns: {e}")
255+
return False
256+
finally:
257+
session.close()

client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"mcp__features__feature_mark_passing",
2525
"mcp__features__feature_skip",
2626
"mcp__features__feature_create_bulk",
27+
"mcp__features__feature_record_failure", # For stuck loop recovery
2728
]
2829

2930
# Playwright MCP tools for browser automation
@@ -144,7 +145,8 @@ def create_client(project_dir: Path, model: str):
144145
],
145146
mcp_servers={
146147
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest", "--viewport-size", "1280x720"]},
147-
"features": {
148+
# "playwright": {"command": "npx", "args": ["@playwright/mcp@latest", "--headless"]},
149+
"features": {
148150
"command": sys.executable, # Use the same Python that's running this script
149151
"args": ["-m", "mcp_server.feature_mcp"],
150152
"env": {

0 commit comments

Comments
 (0)