diff --git a/docs/generated/release-truth.json b/docs/generated/release-truth.json index a467c65..4c532f9 100644 --- a/docs/generated/release-truth.json +++ b/docs/generated/release-truth.json @@ -135,7 +135,7 @@ "console_entrypoints": 8, "mcp_tools": 51, "ops_cli_commands": 5, - "pytest_test_functions": 4123 + "pytest_test_functions": 4126 }, "feature_profile_matrix": { "capture_hook": [ diff --git a/docs/generated/release-truth.md b/docs/generated/release-truth.md index 7c3c60b..15efdf8 100644 --- a/docs/generated/release-truth.md +++ b/docs/generated/release-truth.md @@ -13,7 +13,7 @@ Do not edit this file by hand. Run `python scripts/generate_release_truth.py`. - Main CLI commands: **119** - Operations CLI commands: **5** - Console entrypoints: **8** -- Pytest source test functions: **4123** +- Pytest source test functions: **4126** ## MCP tools diff --git a/memorymaster/dreaming/worker.py b/memorymaster/dreaming/worker.py index 77e4186..03e323d 100644 --- a/memorymaster/dreaming/worker.py +++ b/memorymaster/dreaming/worker.py @@ -177,15 +177,26 @@ def _observation_scope_pairs(self, scope: str | None) -> list[tuple[str, str | N return [(str(row["scope"]), row["tenant_id"]) for row in rows] def _observation_llm(self, system: str, prompt: str) -> str: - from memorymaster.core.llm_provider import call_llm, use_call_scoped_env + """Sintesis de observaciones por el proveedor por defecto. + + Forzaba `MEMORYMASTER_LLM_PROVIDER="opencode"` — el camino GLM que se dio + de baja el 2026-08-20. El consolidador migro a Antigravity ese mismo dia + (`providers.py:build_consolidator`) y este call site quedo atras, mandando + un modelo de Antigravity al proveedor muerto: falla garantizada. + + Se veia en produccion como `opencode: provider call failed code=call_failed` + dos veces por corrida de dreaming, con `graph_observations.failed=2` al + lado, y dejaba jobs de sintesis girando en backoff hasta agotar intentos. + Medido el 2026-08-30: los mismos dos jobs (#11369, #11370) corridos por el + proveedor por defecto arman componente, llaman y parsean sin error, y los + dos devuelven decision=emit. + + No se fija proveedor a proposito: el default es el que sigue la migracion, + asi que la proxima no vuelve a dejar este call site atras. + """ + from memorymaster.core.llm_provider import call_llm - with use_call_scoped_env( - { - "MEMORYMASTER_LLM_PROVIDER": "opencode", - "MEMORYMASTER_LLM_MODEL": self.consolidator.model, - } - ): - return call_llm(system, prompt) + return call_llm(system, prompt) def _run_graph_observations( self, *, owner: str, scope: str | None, synthesize: bool diff --git a/tests/test_observation_llm_uses_live_provider.py b/tests/test_observation_llm_uses_live_provider.py new file mode 100644 index 0000000..9244d34 --- /dev/null +++ b/tests/test_observation_llm_uses_live_provider.py @@ -0,0 +1,63 @@ +"""La sintesis de observaciones no puede clavar un proveedor a mano. + +`_observation_llm` forzaba `MEMORYMASTER_LLM_PROVIDER="opencode"` — el camino +GLM que se dio de baja el 2026-08-20. El consolidador migro a Antigravity ese +mismo dia (`dreaming/providers.py:build_consolidator`) y este call site quedo +atras, mandando un modelo de Antigravity a un proveedor muerto. + +En produccion se veia como `opencode: provider call failed code=call_failed` dos +veces por corrida de dreaming, con `graph_observations.failed=2` al lado, y +dejaba jobs de sintesis girando en backoff hasta agotar los 5 intentos. Medido +el 2026-08-30: los mismos dos jobs corridos por el proveedor por defecto arman +componente, llaman y parsean sin error, los dos con decision=emit. + +El test ancla el REQUISITO ("la sintesis usa el proveedor vigente, no uno +elegido a mano"), no la implementacion: falla si alguien vuelve a envolver la +llamada en un override de proveedor, sin importar cual elija. +""" +from __future__ import annotations + +import inspect + +from memorymaster.dreaming import worker as worker_module + + +def test_no_fija_proveedor_a_mano(): + """Un override aca es como se murio la sintesis por diez dias.""" + fuente = inspect.getsource(worker_module.DreamWorker._observation_llm) + cuerpo = "\n".join( + linea for linea in fuente.splitlines() if not linea.strip().startswith("#") + ) + _, _, codigo = cuerpo.partition('"""') + _, _, codigo = codigo.partition('"""') + + assert "MEMORYMASTER_LLM_PROVIDER" not in codigo, ( + "la sintesis volvio a clavar un proveedor; el default es el que sigue" + " la migracion, un override queda atras y falla en silencio" + ) + assert "use_call_scoped_env" not in codigo, ( + "envolver la llamada en env scopeado es como se colo el override" + ) + + +def test_sigue_llamando_al_proveedor(): + """Complemento del anterior: no vale 'arreglarlo' dejando de llamar.""" + fuente = inspect.getsource(worker_module.DreamWorker._observation_llm) + assert "call_llm(system, prompt)" in fuente + + +def test_el_consolidador_por_defecto_no_es_el_camino_retirado(): + """La migracion del 2026-08-20 que este call site se habia perdido. + + Ojo con "limpiar" mas de la cuenta: `GLMConsolidator` NO es codigo muerto. + `create_dream_consolidator` lo deja ELEGIBLE a proposito — si el plan de + zai-coding-plan vuelve, alcanza con una variable de entorno en vez de un + commit. Lo que estaba muerto era clavar ese proveedor en un call site que + nadie repasa. Este test fija el DEFAULT, no borra la opcion. + """ + from memorymaster.dreaming.providers import create_dream_consolidator + + consolidator = create_dream_consolidator() + nombre = type(consolidator).__name__.lower() + assert "glm" not in nombre, "el default no puede ser el plan dado de baja" + assert "antigravity" in nombre