fix(fallback): turnul nu se mai pierde cand Claude e la limita

Detectia de rate limit functiona, dar `_local_fallback_reply` putea
intoarce None din patru locuri fara nicio linie de log — userul primea
`Claude CLI error (exit 1): You've hit your session limit` pe Discord in
loc de raspuns, si nu se putea afla ulterior din log care branch a picat.

- daca runda de unelte iese goala, turnul se reface fara unelte in loc
  sa fie abandonat
- fiecare return None ramas e logat
- daca nici modelul local nu raspunde, mesajul e „Claude e la limita…"
  plus ora de reset, nu eroarea bruta a CLI-ului
- cron: job-urile heartbeat* tac la limita (last_status: rate_limited);
  celelalte trimit o singura linie scurta, ca sa se vada rularea sarita
- is_rate_limit_error / rate_limit_detail mutate in claude_session.py,
  folosite si de scheduler (care nu poate importa router-ul)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzZAMzbyQbzFdsrVoXijBg
This commit is contained in:
2026-08-23 14:52:44 +00:00
parent a1d39637c8
commit b97edd5184
7 changed files with 147 additions and 17 deletions

View File

@@ -362,6 +362,46 @@ class TestRunJob:
assert "Error" in result
assert sched._jobs[0]["last_status"] == "error"
@pytest.mark.asyncio
async def test_rate_limited_heartbeat_stays_silent(self, sched, tmp_jobs, callback):
"""A limit isn't actionable and recurs every run until it resets —
heartbeats must not narrate it into the channel."""
sched.add_job("heartbeat-4h", "0 * * * *", "ch", "prompt")
mock_proc = MagicMock()
mock_proc.returncode = 1
mock_proc.stdout = ""
mock_proc.stderr = "You've hit your session limit \u00b7 resets 10:50am (UTC)"
with patch("src.scheduler.build_system_prompt", return_value="sys"), \
patch("subprocess.run", return_value=mock_proc):
result = await sched.run_job("heartbeat-4h")
assert result == ""
assert sched._jobs[0]["last_status"] == "rate_limited"
callback.assert_not_awaited()
@pytest.mark.asyncio
async def test_rate_limited_other_job_reports_one_short_line(
self, sched, tmp_jobs, callback
):
sched.add_job("evening-report", "0 * * * *", "ch", "prompt")
mock_proc = MagicMock()
mock_proc.returncode = 1
mock_proc.stdout = ""
mock_proc.stderr = "You've hit your session limit \u00b7 resets 10:50am (UTC)"
with patch("src.scheduler.build_system_prompt", return_value="sys"), \
patch("subprocess.run", return_value=mock_proc):
result = await sched.run_job("evening-report")
assert "Claude e la limită" in result
assert "resets 10:50am (UTC)" in result
assert "Claude CLI error" not in result
assert sched._jobs[0]["last_status"] == "rate_limited"
callback.assert_awaited_once()
@pytest.mark.asyncio
async def test_execute_job_invalid_json(self, sched, tmp_jobs, callback):
sched.add_job("json-err", "0 * * * *", "ch", "prompt")