refactor: remove testing agent claim mechanism for concurrent testing

Remove the testing_in_progress claim/release mechanism from the testing
agent architecture. Multiple testing agents can now test the same feature
concurrently, simplifying the system and eliminating potential stale lock
issues.

Changes:
- parallel_orchestrator.py:
  - Remove claim_feature_for_testing() and release_testing_claim() methods
  - Remove _cleanup_stale_testing_locks() periodic cleanup
  - Replace with simple _get_random_passing_feature() selection
  - Remove startup stale lock cleanup code
  - Remove STALE_TESTING_LOCK_MINUTES constant
  - Remove unused imports (timedelta, text)

- api/database.py:
  - Remove testing_in_progress and last_tested_at columns from Feature model
  - Update to_dict() to exclude these fields
  - Convert _migrate_add_testing_columns() to no-op for backwards compat

- mcp_server/feature_mcp.py:
  - Remove feature_release_testing tool entirely
  - Remove unused datetime import

- prompts.py:
  - Update testing prompt to remove feature_release_testing instruction
  - Testing agents now just verify and exit (no cleanup needed)

- server/websocket.py:
  - Update AgentTracker to use composite keys (feature_id, agent_type)
  - Prevents ghost agent creation from ambiguous [Feature #X] messages
  - Proper separation of coding vs testing agent tracking

Benefits:
- Eliminates artificial bottleneck from claim coordination
- No stale locks to clean up after crashes
- Simpler crash recovery (no testing state to restore)
- Reduced database writes (no claim/release transactions)
- Matches intended design: random, concurrent regression testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-23 15:30:31 +02:00
parent 874359fcf6
commit 486979c3d9
5 changed files with 83 additions and 247 deletions

View File

@@ -73,13 +73,14 @@ ORCHESTRATOR_PATTERNS = {
class AgentTracker:
"""Tracks active agents and their states for multi-agent mode.
Both coding and testing agents are now tracked by their feature ID.
The agent_type field distinguishes between them.
Both coding and testing agents are tracked using a composite key of
(feature_id, agent_type) to allow simultaneous tracking of both agent
types for the same feature.
"""
def __init__(self):
# feature_id -> {name, state, last_thought, agent_index, agent_type}
self.active_agents: dict[int, dict] = {}
# (feature_id, agent_type) -> {name, state, last_thought, agent_index, agent_type}
self.active_agents: dict[tuple[int, str], dict] = {}
self._next_agent_index = 0
self._lock = asyncio.Lock()
@@ -111,14 +112,14 @@ class AgentTracker:
if testing_complete_match:
feature_id = int(testing_complete_match.group(1))
is_success = testing_complete_match.group(2) == "completed"
return await self._handle_agent_complete(feature_id, is_success)
return await self._handle_agent_complete(feature_id, is_success, agent_type="testing")
# Coding agent complete: "Feature #X completed/failed" (without "testing" keyword)
if line.startswith("Feature #") and ("completed" in line or "failed" in line) and "testing" not in line:
try:
feature_id = int(re.search(r'#(\d+)', line).group(1))
is_success = "completed" in line
return await self._handle_agent_complete(feature_id, is_success)
return await self._handle_agent_complete(feature_id, is_success, agent_type="coding")
except (AttributeError, ValueError):
pass
@@ -132,11 +133,21 @@ class AgentTracker:
content = match.group(2)
async with self._lock:
# Ensure agent is tracked
if feature_id not in self.active_agents:
# Check if either coding or testing agent exists for this feature
# This prevents creating ghost agents when a testing agent outputs [Feature #X] lines
coding_key = (feature_id, 'coding')
testing_key = (feature_id, 'testing')
if coding_key in self.active_agents:
key = coding_key
elif testing_key in self.active_agents:
key = testing_key
else:
# Neither exists, create a new coding agent entry (implicit tracking)
key = coding_key
agent_index = self._next_agent_index
self._next_agent_index += 1
self.active_agents[feature_id] = {
self.active_agents[key] = {
'name': AGENT_MASCOTS[agent_index % len(AGENT_MASCOTS)],
'agent_index': agent_index,
'agent_type': 'coding',
@@ -145,7 +156,7 @@ class AgentTracker:
'last_thought': None,
}
agent = self.active_agents[feature_id]
agent = self.active_agents[key]
# Detect state and thought from content
state = 'working'
@@ -178,16 +189,21 @@ class AgentTracker:
return None
async def get_agent_info(self, feature_id: int) -> tuple[int | None, str | None]:
"""Get agent index and name for a feature ID.
async def get_agent_info(self, feature_id: int, agent_type: str = "coding") -> tuple[int | None, str | None]:
"""Get agent index and name for a feature ID and agent type.
Thread-safe method that acquires the lock before reading state.
Args:
feature_id: The feature ID to look up.
agent_type: The agent type ("coding" or "testing"). Defaults to "coding".
Returns:
Tuple of (agentIndex, agentName) or (None, None) if not tracked.
"""
async with self._lock:
agent = self.active_agents.get(feature_id)
key = (feature_id, agent_type)
agent = self.active_agents.get(key)
if agent:
return agent['agent_index'], agent['name']
return None, None
@@ -207,6 +223,7 @@ class AgentTracker:
async def _handle_agent_start(self, feature_id: int, line: str, agent_type: str = "coding") -> dict | None:
"""Handle agent start message from orchestrator."""
async with self._lock:
key = (feature_id, agent_type) # Composite key for separate tracking
agent_index = self._next_agent_index
self._next_agent_index += 1
@@ -216,7 +233,7 @@ class AgentTracker:
if name_match:
feature_name = name_match.group(1)
self.active_agents[feature_id] = {
self.active_agents[key] = {
'name': AGENT_MASCOTS[agent_index % len(AGENT_MASCOTS)],
'agent_index': agent_index,
'agent_type': agent_type,
@@ -237,26 +254,33 @@ class AgentTracker:
'timestamp': datetime.now().isoformat(),
}
async def _handle_agent_complete(self, feature_id: int, is_success: bool) -> dict | None:
"""Handle agent completion - ALWAYS emits a message, even if agent wasn't tracked."""
async def _handle_agent_complete(self, feature_id: int, is_success: bool, agent_type: str = "coding") -> dict | None:
"""Handle agent completion - ALWAYS emits a message, even if agent wasn't tracked.
Args:
feature_id: The feature ID.
is_success: Whether the agent completed successfully.
agent_type: The agent type ("coding" or "testing"). Defaults to "coding".
"""
async with self._lock:
key = (feature_id, agent_type) # Composite key for correct agent lookup
state = 'success' if is_success else 'error'
if feature_id in self.active_agents:
if key in self.active_agents:
# Normal case: agent was tracked
agent = self.active_agents[feature_id]
agent = self.active_agents[key]
result = {
'type': 'agent_update',
'agentIndex': agent['agent_index'],
'agentName': agent['name'],
'agentType': agent.get('agent_type', 'coding'),
'agentType': agent.get('agent_type', agent_type),
'featureId': feature_id,
'featureName': agent['feature_name'],
'state': state,
'thought': 'Completed successfully!' if is_success else 'Failed to complete',
'timestamp': datetime.now().isoformat(),
}
del self.active_agents[feature_id]
del self.active_agents[key]
return result
else:
# Synthetic completion for untracked agent
@@ -265,7 +289,7 @@ class AgentTracker:
'type': 'agent_update',
'agentIndex': -1, # Sentinel for untracked
'agentName': 'Unknown',
'agentType': 'coding',
'agentType': agent_type,
'featureId': feature_id,
'featureName': f'Feature #{feature_id}',
'state': state,