Skip to content

fix(profile): el reduce va por lotes y un run a medias es reanudable - #243

Merged
wolverin0 merged 1 commit into
mainfrom
fix/profile-reduce-batching
Aug 30, 2026
Merged

fix(profile): el reduce va por lotes y un run a medias es reanudable#243
wolverin0 merged 1 commit into
mainfrom
fix/profile-reduce-batching

Conversation

@wolverin0

Copy link
Copy Markdown
Owner

El bloqueo

El reduce pasaba todos los candidatos de un run en una sola llamada, contra un validador (providers.py:108) que exige partición perfecta: cada candidate_id exactamente una vez, sin duplicar ni omitir.

Run Candidatos Desenlace
2 68 completó
3 234 clavado en reducing desde el 2026-08-20

Diez días de corridas programadas más cuatro reintentos medidos a mano, alternando entre profile candidates must appear exactly once y JSON malformado. El perfil inyectado en cada SessionStart quedó con hechos del 6 al 9 de agosto.

Por qué la columna va primero

Lotear sin marcar consumidos habría sido peor que el bloqueo: apply_decisions relee candidates(run_id) entero en cada llamada (repository.py:296), así que un lote aplicado y un crash antes del siguiente dejaba los mismos candidatos listos para aplicarse de nuevo — hechos duplicados en el perfil que entra en cada sesión.

  • Migración 23: compiled_profile_candidates.consumed_at, sin unicidad, índice (run_id, consumed_at). Filas previas en NULL a propósito: un run en vuelo debe ver sus candidatos como pendientes.
  • candidates(run_id, pending_only=True) filtra lo consumido.
  • apply_decisions marca dentro de la misma transacción que aplica la decisión, con AND consumed_at IS NULL.
  • _reduce_and_complete loopea sobre lotes de reduce_batch_size (40, MEMORYMASTER_PROFILE_REDUCE_BATCH), releyendo active_facts() entre lotes para que el lote N+1 fusione contra los hechos del N.
  • Un lote que no consume nada corta con error en vez de loopear infinito.

Semántica, dicho explícito: las fusiones se deciden con visibilidad parcial, así que dos candidatos de lotes distintos pueden quedar como dos hechos donde una partición única los unía. Es el precio, y es preferible a no reducir nunca.

Evidencia en la base viva

El primer intento consumió 120/234 y falló en el cuarto lote — y los tres lotes previos quedaron. Antes ese mismo fallo tiraba todo, que es por qué diez noches seguidas terminaron igual. El reintento siguiente drenó los 114 restantes:

antes después
Run 3 reducing, 10 días completed
Hechos activos 29 52
Viñetas en la proyección 32 50

Techo del perfil: 800/40 → 1400/60

Va en el mismo commit porque el fix lo destapa: con 52 hechos el techo viejo cortaba 20 en silencio y la proyección quedaba clavada en 800/800 — justo cuando los hechos nuevos eran los buenos (memorymaster and UNMS CRM sup=10, WISP, Task Scheduler, ARS) y los viejos los stale (Godot battle royale). Truncar por techo no avisa.

Barra roja

Revirtiendo sólo el filtro de pendientes, la suite se cuelga hasta el timeout en vez de fallar un assert: el bucle relee los mismos 234 para siempre. Ese es exactamente el modo de falla que el filtro evita.

49/49 en tests de perfil, ruff limpio, inyección en SessionStart verificada en vivo (50 viñetas, marcador presente).

Nota: el MCP de GitNexus sigue sin conectar en esta sesión (CONNECTION_CLOSED), así que el radio se midió con grep — candidates() y apply_decisions tienen un solo llamador cada uno dentro de profile/.

El reduce pasaba TODOS los candidatos de un run en una sola llamada contra un
validador que exige particion perfecta: cada candidate_id exactamente una vez,
sin duplicar ni omitir. Eso escala hasta donde el modelo sostiene la particion y
no mas. El run 2 completo con 68 candidatos; el run 3 acumulo 234 y quedo
clavado en `reducing` desde el 2026-08-20 — diez dias de corridas programadas,
mas cuatro reintentos medidos a mano, todos fallando entre "candidates must
appear exactly once" y JSON malformado. El perfil inyectado en cada SessionStart
quedo con hechos del 6 al 9 de agosto.

Lotear sin marcar consumidos habria sido PEOR que el bloqueo: `apply_decisions`
relee los candidatos del run en cada llamada, asi que un lote aplicado y un
crash antes del siguiente dejaba los mismos candidatos listos para aplicarse de
nuevo — hechos duplicados en el perfil. Por eso la columna va primero.

- Migracion 23: `compiled_profile_candidates.consumed_at`, sin unicidad, con
  indice (run_id, consumed_at). Las filas previas quedan NULL a proposito: un
  run en vuelo DEBE ver sus candidatos como pendientes.
- `candidates(run_id, pending_only=True)` filtra lo ya consumido.
- `apply_decisions` marca consumido DENTRO de la misma transaccion que aplica la
  decision, protegido con `AND consumed_at IS NULL`.
- `_reduce_and_complete` loopea sobre lotes de `reduce_batch_size` (40 por
  defecto, `MEMORYMASTER_PROFILE_REDUCE_BATCH`), releyendo `active_facts()`
  entre lotes para que el lote N+1 pueda fusionar contra los hechos del N.
- Un lote que no consume nada corta con error en vez de loopear infinito.

Semantica: las fusiones se deciden con visibilidad parcial, asi que dos
candidatos de lotes distintos pueden quedar como dos hechos donde una particion
unica los unia. Es el precio, y es preferible a no reducir nunca.

MEDIDO EN LA BASE VIVA: el primer intento consumio 120/234 y fallo en el cuarto
lote — y los tres lotes previos QUEDARON. Antes ese mismo fallo tiraba todo, que
es por que diez noches seguidas terminaron igual. El reintento siguiente dreno
los 114 restantes y el run 3 quedo en `completed`. Hechos activos 29 -> 52.

Techo del perfil 800/40 -> 1400/60 en el mismo commit: con 52 hechos el techo
viejo cortaba 20 en silencio y la proyeccion quedaba clavada en 800/800, justo
cuando los hechos NUEVOS eran los buenos (memorymaster+UNMS sup=10, WISP, Task
Scheduler) y los viejos los stale. La proyeccion pasa de 32 a 50 vinietas.

Barra roja verificada: revirtiendo solo el filtro de pendientes, la suite se
cuelga hasta el timeout en vez de fallar un assert — el bucle relee los mismos
234 para siempre. Ese es el modo de falla que el filtro evita.

49/49 en los tests de perfil, ruff limpio, inyeccion en vivo confirmada.
@wolverin0
wolverin0 merged commit 0c7de2c into main Aug 30, 2026
15 checks passed
@wolverin0
wolverin0 deleted the fix/profile-reduce-batching branch August 30, 2026 17:54
wolverin0 added a commit that referenced this pull request Aug 30, 2026


15339 -> 15421 simbolos, 39065 -> 39209 relaciones. Regenerado con
--embeddings para no perder los 12.074 existentes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant