Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
PORT = int(os.getenv("AGENTCHATBUS_PORT", config_data.get("PORT", "39765")))

# Agent heartbeat timeout (seconds). Agents missing this window are marked offline.
AGENT_HEARTBEAT_TIMEOUT = int(os.getenv("AGENTCHATBUS_HEARTBEAT_TIMEOUT", config_data.get("AGENT_HEARTBEAT_TIMEOUT", "30")))
AGENT_HEARTBEAT_TIMEOUT = int(os.getenv("AGENTCHATBUS_HEARTBEAT_TIMEOUT", config_data.get("AGENT_HEARTBEAT_TIMEOUT", "60")))

# SSE long-poll timeout for msg.wait (seconds)
MSG_WAIT_TIMEOUT = int(os.getenv("AGENTCHATBUS_WAIT_TIMEOUT", config_data.get("MSG_WAIT_TIMEOUT", "300")))
Expand Down
6 changes: 4 additions & 2 deletions src/tools/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,8 +1122,10 @@ async def handle_msg_wait(db, arguments: dict[str, Any]) -> list[types.Content]:
)
# ─────────────────────────────────────────────────────────────────────────

# Refresh every 20 seconds to stay online during long-poll waits.
HEARTBEAT_INTERVAL = 20.0
# Refresh every 40 seconds to stay online during long-poll waits.
# LLM-based agents regularly take >20s to respond; 20s was too aggressive
# and caused unnecessary heartbeat DB writes. Paired with AGENT_HEARTBEAT_TIMEOUT=60s.
HEARTBEAT_INTERVAL = 40.0

async def _refresh_heartbeat() -> None:
if verified_agent:
Expand Down
23 changes: 9 additions & 14 deletions tests/test_timeout_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def custom_showwarning(self, message, category, filename, lineno, file=None, lin
@pytest.mark.asyncio
async def test_api_threads_timeout_on_get_db():
"""Test that API returns 503 when get_db() times out."""
with patch("asyncio.wait_for") as mock_wait_for:
with patch("src.main.asyncio.wait_for") as mock_wait_for:
# First call to wait_for (get_db) times out
mock_wait_for.side_effect = asyncio.TimeoutError()

Expand All @@ -98,7 +98,7 @@ async def mock_wait_for_impl(coro, timeout):
else:
raise asyncio.TimeoutError()

with patch("asyncio.wait_for", side_effect=mock_wait_for_impl):
with patch("src.main.asyncio.wait_for", side_effect=mock_wait_for_impl):
try:
await api_threads()
pytest.fail("Expected HTTPException with 503")
Expand All @@ -113,7 +113,7 @@ async def test_api_agents_timeout():
async def mock_wait_for_impl(coro, timeout):
raise asyncio.TimeoutError()

with patch("asyncio.wait_for", side_effect=mock_wait_for_impl):
with patch("src.main.asyncio.wait_for", side_effect=mock_wait_for_impl):
try:
await api_agents()
pytest.fail("Expected HTTPException with 503")
Expand All @@ -129,7 +129,6 @@ async def mock_wait_for_impl(coro, timeout):
@pytest.mark.asyncio
async def test_api_threads_success():
"""Test successful thread listing with no timeout."""
mock_db = AsyncMock()
import datetime
now = datetime.datetime.now()

Expand All @@ -145,16 +144,12 @@ async def test_api_threads_success():
)
]

async def mock_wait_for_get_db(coro, timeout):
return mock_db

async def mock_gather(*coros):
return (mock_threads, len(mock_threads))
mock_db = AsyncMock()

with patch("asyncio.wait_for", side_effect=mock_wait_for_get_db), \
patch("asyncio.gather", side_effect=mock_gather):
# Since api_threads is an async function that returns an envelope dict,
# we need to test the actual return value
with patch("src.main.get_db", return_value=mock_db), \
patch("src.main.crud.thread_list", new=AsyncMock(return_value=mock_threads)), \
patch("src.main.crud.thread_count", new=AsyncMock(return_value=len(mock_threads))), \
patch("src.main.crud.threads_agents_map", new=AsyncMock(return_value={})):
result = await api_threads()

# Verify result is an envelope dict with expected structure (UP-20)
Expand Down Expand Up @@ -195,7 +190,7 @@ async def mock_wait_for_impl(coro, timeout):
# Return mock_agents for agent_list calls
return mock_agents

with patch("asyncio.wait_for", side_effect=mock_wait_for_impl):
with patch("src.main.asyncio.wait_for", side_effect=mock_wait_for_impl):
result = await api_agents()

assert isinstance(result, list)
Expand Down