Skip to content

Commit 1a9bf71

Browse files
coordtclaude
andcommitted
Drain all queued tasks on agent startup (loop until empty)
Task 3 from pr-21-fixes.md: - _lifespan startup poll now loops calling next_task() until it returns None, processing each task before moving to the next; previously only one task was claimed, leaving N-1 accumulated tasks permanently stuck - New test: startup poll with 3 queued tasks drains all 3 before yield Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0712ad8 commit 1a9bf71

2 files changed

Lines changed: 29 additions & 5 deletions

File tree

agents/issue-triage/issue_triage/agent.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,20 @@ async def _poll_and_process(client: ForemanClient) -> None:
7373

7474
@asynccontextmanager
7575
async def _lifespan(application: FastAPI) -> AsyncIterator[None]:
76-
"""FastAPI lifespan: startup poll for tasks queued while the agent was down.
76+
"""FastAPI lifespan: drain all tasks queued while the agent was down.
77+
78+
Loops calling next_task() until the queue is empty so that accumulated
79+
pending tasks are not left stuck after an unclean restart.
7780
7881
Args:
7982
application: The FastAPI application instance.
8083
"""
8184
client = _get_client(application)
82-
await _poll_and_process(client)
85+
while True:
86+
task = await asyncio.to_thread(client.next_task)
87+
if task is None:
88+
break
89+
await _process_task(client, task)
8390
yield
8491
client.close()
8592

tests/test_agent_server.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,29 @@ def test_next_task_returning_task_calls_complete(
141141

142142

143143
class TestStartupPoll:
144-
"""Lifespan startup poll fires next_task() once on boot."""
144+
"""Lifespan startup poll drains all queued tasks on boot."""
145145

146-
def test_startup_poll_calls_next_task_once(self, mock_foreman_client: MagicMock) -> None:
147-
"""Lifespan startup poll calls client.next_task() exactly once."""
146+
def test_startup_poll_calls_next_task_once_when_queue_empty(self, mock_foreman_client: MagicMock) -> None:
147+
"""Startup poll calls next_task() once when the queue is empty (returns None)."""
148148
app.state.client = mock_foreman_client
149149
with TestClient(app):
150150
pass
151151
del app.state.client
152152
assert mock_foreman_client.next_task.call_count == 1
153+
154+
def test_startup_poll_drains_all_queued_tasks(self, mock_foreman_client: MagicMock, mocker) -> None:
155+
"""Startup poll loops until next_task() returns None, processing each task."""
156+
tasks = [_make_task(f"t{i}") for i in range(3)]
157+
mock_foreman_client.next_task.side_effect = [*tasks, None]
158+
stub_decision = MagicMock()
159+
mocker.patch("agent.triage", return_value=stub_decision)
160+
161+
app.state.client = mock_foreman_client
162+
with TestClient(app):
163+
pass
164+
del app.state.client
165+
166+
# next_task called 4 times: 3 tasks + 1 None to break the loop
167+
assert mock_foreman_client.next_task.call_count == 4
168+
# All 3 tasks completed
169+
assert mock_foreman_client.complete_task.call_count == 3

0 commit comments

Comments
 (0)