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
93 changes: 93 additions & 0 deletions .planning/HANDOFF-2026-08-31.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# HANDOFF — memorymaster, 2026-08-31
# Que cubre: estado al cierre del dia, los 8 PRs que entraron, lo que quedo
# abierto con su evidencia, y las trampas que costaron tiempo hoy.
# Leer cuando: retomes este pane, o antes de tocar perfil compilado / PPR-7 / CI.
# Estado: main limpio, 0 PRs abiertos salvo #250 esperando CI.

## Donde quedo todo

`main` en `ded7dbc` + lo que entre del #250. Worktrees: 2 (el canonico y
`G:/tmp/memorymaster-workflow-intelligence`, que ya se puede borrar: su rama
esta mergeada). Ramas locales: 51. Stashes: 0.

## Lo que entro hoy

| PR | Que arreglo |
|----|-------------|
| #243 | El reduce del perfil va por lotes y un run a medias es reanudable |
| #244 | La sintesis de observaciones usa el proveedor vigente, no uno clavado |
| #245 | Guard: un `except` que escribe un codigo constante debe guardar la causa |
| #246 | Mutacion sobre ese guard; encontro un test que pasaba por el motivo equivocado |
| #247 | Reindexar GitNexus no puede borrar los embeddings, y se verifica al final |
| #248 | Workflow Intelligence v1 (revisado, no escrito por mi) — apagado por defecto |
| #249 | `release-truth` dejo de colgar del job de 70 minutos |
| #250 | El flaky de hermes: la espera de arranque no esperaba (en CI al cierre) |

## Resultados medidos, no declarados

- **Perfil compilado destrabado** tras 10 dias clavado. Hechos 29 -> 52,
vinietas inyectadas 32 -> 50, techo 800/40 -> 1400/60 (el viejo cortaba 20
hechos en silencio).
- **PPR-7 volvio a emitir**: `synthesis_completed=2, emitted=2, failed=0`.
Primera emision desde el 2026-08-14.
- **Corpus curado**: 13.767 claims agrupadas por tema, 18% -> 96%.
- **Tenant**: cero claims VIVAS en NULL. 4.783 clones exactos archivados
apuntando a su original.
- **Migracion 0024 aplicada por la via gobernada**: tabla creada, 0 filas
(sin backfill), 147.335 claims intactas, quick_check ok, 0 FKs rotas.
- **Scan metadata-only**: 15.444 sesiones, 20.064 archivos, 0 errores, nada
activado (`candidates: 0`, `reviews: 0`).

## Abierto, con evidencia y sin dueno asignado

1. **`MemoryMaster-Dreaming` sale 1** con `errors: 1` en las dos ultimas
corridas. NO es el perfil (`not_due`, sano) ni las observaciones
(`failed: 0`). Esta en el camino de extraccion/consolidacion del dream.
Nadie lo persiguio todavia.
2. **El recall opt-in de observaciones entrega CERO**, y no por un bug del
camino: no hay ninguna observacion confirmada (2 archivadas + 1 candidata).
Medido corriendo `query_context_bundle` con y sin `include_observations`.
3. **96,3% del discovery de PPR-7 termina en `no_supports`** (4.549 de 4.724 en
7 dias). El embudo esta ANTES de la sintesis, no en ella.
4. **El perfil tiene 11 dias de atraso**: `last_supported_at` no pasa del
2026-08-20, donde termino el watermark del run 3.
5. **Precision de observaciones: NO MEDIBLE** (n=2). Se repite el veredicto del
2026-08-24 con n=0. Reportar 100% sobre dos casos seria inventar un numero.
6. **Expiracion de preferencias NO ejercitada**: 10 hechos `preference`, todos
de agosto, TTL 90 dias.
7. **Hermes sync: verde solo por exit code.** No tiene log de resultado.
8. **Namespace de snapshots vacio** (`no snapshots found`): el rollback nunca se
ejercito y no hay a donde volver.
9. **Campo viejo sin limpiar**: el run 3 del perfil quedo con
`error_code='ProfileValidationError'` pese a `status='completed'`.

## Trampas que costaron tiempo hoy (para no repetirlas)

- **`release-truth` cuenta funciones de test.** Agregar una sin regenerar deja
el archivo viejo y rompe el CI. Cai CUATRO veces. El #249 hace que ahora te
lo diga un job de 30 segundos en vez de seis jobs y 70 minutos.
- **Con un archivo generado en el diff, MERGE le gana a REBASE.** El rebase
multiplica el conflicto por la cantidad de commits; hoy me dejo en HEAD
desacoplado con 33 de 43 archivos. El merge lo presenta una vez, sobre el
estado final, que es el unico que importa.
- **Un fallo de CI se clasifica nombrando el test, no adivinando.** Y la
pregunta que decide si relanzar es si existe un MECANISMO por el que el
cambio pueda causarlo: en el #247 pude probar que no existia (el archivo no
lo importa nadie); en el #248 existia y era real.
- **Un `failed: 0` puede significar "no se intento nada".** Los 3 jobs de PPR-7
estaban `blocked` en el tope, asi que el fix no podia demostrarse. Casi lo
reporto como verde.
- **`grep` de un substring miente sobre el codigo**: `'opencode' in src` me dio
falso positivo por mi PROPIO docstring explicando la historia.
- **Los blobs base64 de un transcript matchean regexes de palabras.** Conte
"GPL 25 veces" y eran datos codificados; casi bloqueo un merge por licencia
sobre esa base.

## Reglas nuevas que quedaron como codigo

- `scripts/check_swallowed_cause.py` — un `except` no puede escribir una
etiqueta constante y tirar la excepcion. 4 hallazgos reales, 0 falsos.
- `scripts/mutate_swallowed_cause.py` — 6 mutaciones sobre ese guard; imprime
QUE test mato cada una, que es lo unico que hizo visible el test tramposo.
- `scripts/gitnexus_reindex.py` — reindexar sin `--embeddings` los BORRA (hoy
12.079). Verifica el conteo DESPUES de correr, contra el valor previo.
2 changes: 1 addition & 1 deletion docs/generated/release-truth.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"console_entrypoints": 9,
"mcp_tools": 51,
"ops_cli_commands": 5,
"pytest_test_functions": 4177
"pytest_test_functions": 4182
},
"feature_profile_matrix": {
"capture_hook": [
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/release-truth.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Do not edit this file by hand. Run `python scripts/generate_release_truth.py`.
- Main CLI commands: **120**
- Operations CLI commands: **5**
- Console entrypoints: **9**
- Pytest source test functions: **4177**
- Pytest source test functions: **4182**

## MCP tools

Expand Down
97 changes: 97 additions & 0 deletions tests/test_hermes_http_fixture_wait.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""La espera de arranque del servidor MCP tiene que esperar de verdad.

El bucle original dormia SOLO en el `except`. Si el servidor aceptaba la
conexion pero devolvia un estado distinto de 200 —el caso normal mientras
arranca— giraba sus 100 vueltas en milisegundos y se rendia sin haber esperado
nada. Y con un limite en vueltas y no en tiempo, el presupuesto real dependia de
si cada intento fallaba rapido o agotaba su timeout: no habia presupuesto.

El sintoma no era un error de arranque legible. El test avanzaba, la llamada MCP
fallaba, y `_classify_transport_error` la reportaba como `authority_unavailable`
— su fallback para cualquier error de transporte sin clasificar. Cuatro caidas
en CI el 2026-08-31, todas en Windows, entre 1 y 1,5 horas de suite cada una.

Estos tests anclan el REQUISITO ("la espera consume su presupuesto de tiempo
antes de rendirse"), no la implementacion. Y fijan explicitamente que esto NO es
un reintento: no repite aserciones ni tolera un fallo persistente.
"""
from __future__ import annotations

import time

import httpx

from test_hermes_memory_provider_http import _wait_until_healthy


class _Resp:
def __init__(self, status_code: int) -> None:
self.status_code = status_code


def test_duerme_tambien_cuando_responde_distinto_de_200(monkeypatch):
"""El bug exacto: sin sleep en esta rama, el presupuesto se evapora."""
llamadas = {"get": 0, "sleep": 0.0}

def fake_get(url, timeout=None):
llamadas["get"] += 1
return _Resp(503)

def fake_sleep(seconds):
llamadas["sleep"] += seconds

monkeypatch.setattr(httpx, "get", fake_get)
monkeypatch.setattr(time, "sleep", fake_sleep)

assert _wait_until_healthy("http://x/healthz", budget_seconds=0.3) is False
assert llamadas["get"] > 0
assert llamadas["sleep"] > 0, (
"giro sin dormir ante un 503: es el defecto que dejaba el arranque sin espera"
)


def test_devuelve_true_apenas_hay_200(monkeypatch):
estados = [503, 503, 200]

monkeypatch.setattr(httpx, "get", lambda url, timeout=None: _Resp(estados.pop(0)))
monkeypatch.setattr(time, "sleep", lambda s: None)

assert _wait_until_healthy("http://x/healthz", budget_seconds=5.0) is True
assert not estados, "no consumio los intentos previos"


def test_tolera_errores_de_transporte_mientras_arranca(monkeypatch):
intentos = {"n": 0}

def fake_get(url, timeout=None):
intentos["n"] += 1
if intentos["n"] < 3:
raise httpx.ConnectError("connection refused")
return _Resp(200)

monkeypatch.setattr(httpx, "get", fake_get)
monkeypatch.setattr(time, "sleep", lambda s: None)

assert _wait_until_healthy("http://x/healthz", budget_seconds=5.0) is True


def test_el_limite_es_TIEMPO_y_se_respeta(monkeypatch):
"""Sin limite temporal no hay presupuesto: 100 vueltas duran lo que duren."""
reloj = {"t": 0.0}
monkeypatch.setattr(time, "monotonic", lambda: reloj["t"])
monkeypatch.setattr(time, "sleep", lambda s: reloj.__setitem__("t", reloj["t"] + s))
monkeypatch.setattr(httpx, "get", lambda url, timeout=None: _Resp(500))

inicio = reloj["t"]
assert _wait_until_healthy("http://x/h", budget_seconds=2.0, poll_seconds=0.1) is False
consumido = reloj["t"] - inicio
assert 2.0 <= consumido < 2.5, f"no consumio su presupuesto: {consumido}"


def test_no_es_un_reintento_del_test(monkeypatch):
"""Un servidor que NUNCA levanta sigue fallando: no tapa una caida real."""
monkeypatch.setattr(httpx, "get", lambda url, timeout=None: (_ for _ in ()).throw(
httpx.ConnectError("nunca levanta")))
monkeypatch.setattr(time, "sleep", lambda s: None)

assert _wait_until_healthy("http://x/h", budget_seconds=0.2) is False
39 changes: 32 additions & 7 deletions tests/test_hermes_memory_provider_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,37 @@ def _free_port() -> int:
return int(probe.getsockname()[1])


def _wait_until_healthy(
health_url: str, *, budget_seconds: float = 30.0, poll_seconds: float = 0.05
) -> bool:
"""Espera a que el servidor responda 200, durmiendo en TODOS los caminos.

El bucle anterior dormia solo en el `except`: si el servidor aceptaba la
conexion pero devolvia un estado distinto de 200 —arrancando— giraba sus 100
vueltas en milisegundos y se rendia sin haber esperado nada. Y con 100
iteraciones fijas el presupuesto real dependia de si cada intento fallaba
rapido o agotaba su timeout, o sea que no habia presupuesto.

Ahora el limite es TIEMPO, no vueltas. Importa porque el sintoma no es un
error de arranque legible: el test avanza, la llamada MCP falla, y
`_classify_transport_error` la reporta como `authority_unavailable`, que es
su fallback para cualquier error de transporte sin clasificar. Cuatro caidas
en CI el 2026-08-31, todas en Windows, entre 1 y 1,5 h de suite cada una.

Esto NO es un reintento del test: no repite aserciones ni tolera un fallo
real. Solo le da al arranque el tiempo que el bucle decia darle y no daba.
"""
deadline = time.monotonic() + budget_seconds
while time.monotonic() < deadline:
try:
if httpx.get(health_url, timeout=1.0).status_code == 200:
return True
except httpx.HTTPError:
pass
time.sleep(poll_seconds)
return False


@pytest.fixture
def mcp_http_server(tmp_path: Path):
workspace = tmp_path / "workspace"
Expand Down Expand Up @@ -91,13 +122,7 @@ def mcp_http_server(tmp_path: Path):
creationflags=flags,
)
health = f"http://127.0.0.1:{port}/healthz"
for _ in range(100):
try:
if httpx.get(health, timeout=0.2).status_code == 200:
break
except httpx.HTTPError:
time.sleep(0.02)
else:
if not _wait_until_healthy(health):
raise AssertionError("disposable MemoryMaster MCP server did not start")
yield f"http://127.0.0.1:{port}/mcp", token, db, workspace
process.terminate()
Expand Down
Loading