Skip to content

Commit 2e7ffe7

Browse files
Merge pull request #1335 from MervinPraison/claude/issue-1330-20260409-0928
fix: resolve three critical architectural gaps in error handling and resource management
2 parents 40f8d0d + f74ee5d commit 2e7ffe7

File tree

12 files changed

+1108
-20
lines changed

12 files changed

+1108
-20
lines changed

.github/workflows/auto-pr-comment.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ jobs:
409409
permissions:
410410
pull-requests: write
411411
steps:
412-
- name: Trigger CodeRabbit, Qodo, and Gemini reviews
412+
- name: Trigger CodeRabbit and Qodo reviews
413413
uses: actions/github-script@v7
414414
with:
415415
github-token: ${{ secrets.GH_TOKEN }}
@@ -432,12 +432,12 @@ jobs:
432432
});
433433
console.log(`Triggered Qodo for bot PR #${pr}`);
434434
435-
// 3. Trigger Gemini review
436-
await github.rest.issues.createComment({
437-
issue_number: pr, owner, repo,
438-
body: '@gemini review this PR'
439-
});
440-
console.log(`Triggered Gemini for bot PR #${pr}`);
435+
// 3. Trigger Gemini review (commented out)
436+
// await github.rest.issues.createComment({
437+
// issue_number: pr, owner, repo,
438+
// body: '@gemini review this PR'
439+
// });
440+
// console.log(`Triggered Gemini for bot PR #${pr}`);
441441
442442
// NOTE: @copilot is NOT triggered here.
443443
// It will be triggered by copilot-after-coderabbit or copilot-after-qodo

src/praisonai-agents/.agent/workflows/review-chain.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ PR opened
1111
1212
CodeRabbit (@coderabbitai) ─── auto for human PRs, triggered via comment for bot PRs
1313
Qodo (/review) ─── auto for human PRs, triggered via comment for bot PRs
14-
Gemini (@gemini) ─── triggered via comment for bot PRs/Issues
15-
↓ (~3-5 min) ↓ (auto triggers Claude on completion)
14+
<!-- Gemini (@gemini) ─── triggered via comment for bot PRs/Issues
15+
↓ (~3-5 min) ↓ (auto triggers Claude on completion) -->
1616
Copilot (@copilot) ─── triggered ONLY after CodeRabbit or Qodo post their review
1717
1818
Claude (@claude) ─── triggered ONLY after Copilot OR Gemini finishes (final reviewer)

src/praisonai-agents/praisonaiagents/agent/agent.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4464,6 +4464,146 @@ def _get_tools_cache_key(self, tools):
44644464
@contextlib.contextmanager
44654465

44664466

4467+
# -------------------------------------------------------------------------
4468+
# Resource Lifecycle Management
4469+
# -------------------------------------------------------------------------
4470+
4471+
def close(self) -> None:
4472+
"""Synchronously close the agent and clean up resources."""
4473+
if getattr(self, '_closed', False):
4474+
return
4475+
4476+
# Memory cleanup
4477+
try:
4478+
memory = getattr(self, "_memory_instance", None)
4479+
if memory and hasattr(memory, 'close_connections'):
4480+
memory.close_connections()
4481+
except Exception as e:
4482+
logger.warning(f"Memory cleanup failed: {e}")
4483+
4484+
# MCP cleanup
4485+
try:
4486+
if hasattr(self, '_mcp_clients') and self._mcp_clients:
4487+
for client_name, client in self._mcp_clients.items():
4488+
if hasattr(client, 'close'):
4489+
client.close()
4490+
self._mcp_clients.clear()
4491+
except Exception as e:
4492+
logger.warning(f"MCP cleanup failed: {e}")
4493+
4494+
# Server registry cleanup
4495+
try:
4496+
self._cleanup_server_registrations()
4497+
except Exception as e:
4498+
logger.warning(f"Server cleanup failed: {e}")
4499+
4500+
# Background tasks cleanup
4501+
try:
4502+
for task in getattr(self, '_background_tasks', []):
4503+
if hasattr(task, 'cancel'):
4504+
task.cancel()
4505+
except Exception as e:
4506+
logger.warning(f"Task cleanup failed: {e}")
4507+
4508+
# Always set closed flag
4509+
self._closed = True
4510+
4511+
async def aclose(self) -> None:
4512+
"""Async version of close() for async context managers."""
4513+
try:
4514+
# Close memory connections asynchronously if supported
4515+
if hasattr(self, 'memory') and self.memory:
4516+
if hasattr(self.memory, 'aclose'):
4517+
await self.memory.aclose()
4518+
elif hasattr(self.memory, 'close_connections'):
4519+
self.memory.close_connections()
4520+
4521+
# Close MCP sessions asynchronously if supported
4522+
if hasattr(self, '_mcp_clients') and self._mcp_clients:
4523+
for client in self._mcp_clients.values():
4524+
if hasattr(client, 'aclose'):
4525+
await client.aclose()
4526+
elif hasattr(client, 'close'):
4527+
client.close()
4528+
self._mcp_clients.clear()
4529+
4530+
# Clean up server registrations and tasks
4531+
self._cleanup_server_registrations()
4532+
4533+
if hasattr(self, '_background_tasks'):
4534+
for task in self._background_tasks:
4535+
if hasattr(task, 'cancel'):
4536+
task.cancel()
4537+
# Wait for cancellation to complete
4538+
try:
4539+
await task
4540+
except asyncio.CancelledError:
4541+
pass
4542+
4543+
self._closed = True
4544+
4545+
except Exception as e:
4546+
logger.warning(f"Error during async agent cleanup: {e}")
4547+
4548+
def _cleanup_server_registrations(self) -> None:
4549+
"""Clean up global server registry entries for this agent."""
4550+
try:
4551+
agent_id = self.agent_id
4552+
with _server_lock:
4553+
# Remove from _registered_agents
4554+
ports_to_clean = []
4555+
for port, path_dict in _registered_agents.items():
4556+
paths_to_remove = []
4557+
for path, registered_id in path_dict.items():
4558+
if registered_id == agent_id:
4559+
paths_to_remove.append(path)
4560+
4561+
for path in paths_to_remove:
4562+
del path_dict[path]
4563+
4564+
# If no paths left for this port, mark port for cleanup
4565+
if not path_dict:
4566+
ports_to_clean.append(port)
4567+
4568+
# Clean up empty port entries
4569+
for port in ports_to_clean:
4570+
_registered_agents.pop(port, None)
4571+
_server_started.pop(port, None)
4572+
# Note: We don't clean up _shared_apps here as other agents might be using them
4573+
4574+
except Exception as e:
4575+
logger.warning(f"Error cleaning up server registrations: {e}")
4576+
4577+
def __enter__(self):
4578+
"""Context manager entry."""
4579+
return self
4580+
4581+
def __exit__(self, exc_type, exc_val, exc_tb):
4582+
"""Context manager exit - clean up resources."""
4583+
self.close()
4584+
4585+
async def __aenter__(self):
4586+
"""Async context manager entry."""
4587+
return self
4588+
4589+
async def __aexit__(self, exc_type, exc_val, exc_tb):
4590+
"""Async context manager exit - clean up resources."""
4591+
await self.aclose()
4592+
4593+
def __del__(self):
4594+
"""Destructor - ensure resources are cleaned up."""
4595+
try:
4596+
if not getattr(self, '_closed', False):
4597+
self.close()
4598+
except Exception:
4599+
# Ignore errors in destructor to avoid issues during shutdown
4600+
pass
4601+
4602+
@property
4603+
def is_closed(self) -> bool:
4604+
"""Returns True if the agent has been closed."""
4605+
return getattr(self, '_closed', False)
4606+
44674607
def __str__(self):
44684608
return f"Agent(name='{self.name}', role='{self.role}', goal='{self.goal}')"
44694609

src/praisonai-agents/praisonaiagents/agent/execution_mixin.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ def _get_display_functions():
5454
class ExecutionMixin:
5555
"""Mixin providing execution methods for the Agent class."""
5656

57+
def _safe_sleep(self, duration: float) -> None:
58+
"""Synchronous sleep - safe for sync contexts only."""
59+
time.sleep(duration)
60+
61+
async def _safe_sleep_async(self, duration: float) -> None:
62+
"""Async sleep - use this in async contexts."""
63+
await asyncio.sleep(duration)
64+
5765
def generate_task(self) -> 'Task':
5866
"""Generate a Task object from the agent's instructions"""
5967
from ..task.task import Task
@@ -954,10 +962,10 @@ def run_server():
954962
server_thread.start()
955963

956964
# Wait for a moment to allow the server to start and register endpoints
957-
time.sleep(0.5)
965+
self._safe_sleep(0.5)
958966
else:
959967
# If server is already running, wait a moment to make sure the endpoint is registered
960-
time.sleep(0.1)
968+
self._safe_sleep(0.1)
961969
print(f"🔌 Available endpoints on port {port}: {', '.join(list(_registered_agents[port].keys()))}")
962970

963971
# Get the stack frame to check if this is the last launch() call in the script
@@ -986,7 +994,7 @@ def run_server():
986994
try:
987995
print("\nAll agents registered for HTTP mode. Press Ctrl+C to stop the servers.")
988996
while True:
989-
time.sleep(1)
997+
self._safe_sleep(1)
990998
except KeyboardInterrupt:
991999
print("\nServers stopped")
9921000
except Exception as e:
@@ -995,7 +1003,7 @@ def run_server():
9951003
try:
9961004
print("\nKeeping HTTP servers alive. Press Ctrl+C to stop.")
9971005
while True:
998-
time.sleep(1)
1006+
self._safe_sleep(1)
9991007
except KeyboardInterrupt:
10001008
print("\nServers stopped")
10011009
return None
@@ -1055,12 +1063,12 @@ def run_mcp_server():
10551063

10561064
server_thread = threading.Thread(target=run_mcp_server, daemon=True)
10571065
server_thread.start()
1058-
time.sleep(0.5)
1066+
self._safe_sleep(0.5)
10591067

10601068
try:
10611069
print("\nKeeping MCP server alive. Press Ctrl+C to stop.")
10621070
while True:
1063-
time.sleep(1)
1071+
self._safe_sleep(1)
10641072
except KeyboardInterrupt:
10651073
print("\nMCP Server stopped")
10661074
return None

src/praisonai-agents/praisonaiagents/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ def _rich():
2424

2525
# Logging is already configured in _logging.py via __init__.py
2626

27-
# Global list to store error logs
28-
error_logs = []
27+
# Global list to store error logs - with bounded size to prevent memory leaks
28+
from collections import deque
29+
MAX_ERROR_LOGS = 1000 # Maximum number of error logs to keep in memory
30+
error_logs = deque(maxlen=MAX_ERROR_LOGS) # Ring buffer that auto-evicts old errors
2931
_error_logs_lock = threading.Lock()
3032

3133
# Separate registries for sync and async callbacks

src/praisonai-agents/praisonaiagents/memory/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ def __getattr__(name):
106106
if name == "repeat":
107107
from ..workflows import repeat
108108
return repeat
109+
# Structured result types for better error handling
110+
if name == "MemoryResult":
111+
from .results import MemoryResult
112+
return MemoryResult
113+
if name == "MemoryResultStatus":
114+
from .results import MemoryResultStatus
115+
return MemoryResultStatus
116+
if name == "SearchResult":
117+
from .results import SearchResult
118+
return SearchResult
109119
# Backward compatibility aliases
110120
if name == "StepInput":
111121
from ..workflows import WorkflowContext

0 commit comments

Comments
 (0)