mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Two remaining 120s-wall consumers found tracing live 504s on the
redeployed stack (claim_review completing at exactly 120s with a
~74s silent tail after the bounded evidence legs gave up):
_institutional_memory's similar_memory call rode the Ollama
embedder's 120s read timeout unbounded inside every full briefing
(give_me_work / i_will_work_on / i_will_plan / resume pass task=t),
so a saturated embedder could eat a claim verb's whole budget. It
now runs under asyncio.wait_for with the new
institutional_memory_timeout_seconds (default 8s) and degrades to a
first-class {status: "timeout", lessons: []} block — the briefing
is enrichment, never the thing that kills a verb.
claim_review's advisory conventions leg trusted that the diff /
files_changed legs had warmed the workspace; when those legs timed
out, conventions_check_for_task's unwrapped setup phase re-hit the
same cold/contended git ops (30s-bounded subprocesses in sequence,
clone repair up to 300s) and ground on until the verb wall. The
evidence build now skips the conventions leg whenever the git legs
already recorded gaps, emitting the existing could_not_run shape
plus an evidence_gaps note instead of a 504. Fail-closed
enforcement (i_am_done / pr_pass) is untouched.
Gate: 15481 passed, 459 skipped; ruff/mypy/xenon/vulture/bandit/
pip-audit/deptry/import-linter/foundation-check green.
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""``_institutional_memory``'s RAG search is bounded by
|
|
``institutional_memory_timeout_seconds`` — a saturated Ollama embedder must
|
|
never eat the whole verb timeout budget and 504 a claim. See
|
|
``roboco.services.gateway.choreographer._impl.Choreographer._institutional_memory``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.config import Settings
|
|
from roboco.services.gateway.choreographer import Choreographer
|
|
|
|
_DEFAULT_TIMEOUT = 8.0
|
|
_OVERRIDE_TIMEOUT = 3.5
|
|
|
|
|
|
def _choreographer(*, similar_memory: AsyncMock) -> Choreographer:
|
|
repo = AsyncMock()
|
|
repo.similar_memory = similar_memory
|
|
task_svc = AsyncMock()
|
|
task_svc.agent_for.return_value = MagicMock(role="developer")
|
|
choreo = object.__new__(Choreographer)
|
|
choreo._deps = MagicMock(evidence_repo=repo, task=task_svc)
|
|
return choreo
|
|
|
|
|
|
def _task() -> MagicMock:
|
|
return MagicMock(
|
|
id=uuid4(), title="Add retry backoff", task_type=MagicMock(value="code")
|
|
)
|
|
|
|
|
|
class TestInstitutionalMemoryTimeout:
|
|
@pytest.mark.asyncio
|
|
async def test_slow_search_times_out_without_raising(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
|
|
monkeypatch.setattr(
|
|
"roboco.config.settings.institutional_memory_timeout_seconds", 0.01
|
|
)
|
|
|
|
async def _slow(**_kwargs: object) -> dict[str, object]:
|
|
await asyncio.sleep(5)
|
|
return {"items": [], "status": "ok"}
|
|
|
|
choreo = _choreographer(similar_memory=AsyncMock(side_effect=_slow))
|
|
result = await choreo._institutional_memory(uuid4(), _task())
|
|
assert result == {"status": "timeout", "lessons": []}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fast_search_flows_through_unchanged(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
|
|
monkeypatch.setattr(
|
|
"roboco.config.settings.institutional_memory_timeout_seconds", 8.0
|
|
)
|
|
lesson = {"kind": "learning", "summary": "s", "source": "src", "score": 0.9}
|
|
similar_memory = AsyncMock(return_value={"items": [lesson], "status": "ok"})
|
|
choreo = _choreographer(similar_memory=similar_memory)
|
|
result = await choreo._institutional_memory(uuid4(), _task())
|
|
assert result == {"status": "ok", "lessons": [lesson]}
|
|
|
|
|
|
class TestInstitutionalMemoryTimeoutConfig:
|
|
def test_default_is_eight_seconds(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("ROBOCO_INSTITUTIONAL_MEMORY_TIMEOUT_SECONDS", raising=False)
|
|
assert Settings().institutional_memory_timeout_seconds == _DEFAULT_TIMEOUT
|
|
|
|
def test_reads_env_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv(
|
|
"ROBOCO_INSTITUTIONAL_MEMORY_TIMEOUT_SECONDS", str(_OVERRIDE_TIMEOUT)
|
|
)
|
|
assert Settings().institutional_memory_timeout_seconds == _OVERRIDE_TIMEOUT
|