Dejé de Reexplicarle lo Mismo a Cada Agente: Memoria Colaborativa, Skills y Embeddings en ProjectHub
Llegué a un punto con tantos agentes —Cursor, Claude Code, Codex, OpenClaw, opencode, Copilot— que cada vez que cambiaba de uno tenía que volver a darle el mismo host SSH, las mismas credenciales, las mismas instrucciones. Así nació la capa de memoria de ProjectHub: una memoria compartida, buscable por significado, con secretos enmascarados, que convirtió al agente en algo intercambiable y que hoy mi equipo entero consulta sin que yo explique nada.
Dejé de Reexplicarle lo Mismo a Cada Agente: Memoria Colaborativa, Skills y Embeddings en ProjectHub
Llegó un día en que tenía demasiados agentes. Cursor, Claude Code, Codex, OpenClaw, opencode, GitHub Copilot. Cada uno bueno para algo. Y cada uno, una amnesia distinta.
El problema no era ninguno de ellos en particular. Era el ritual. Cada vez que cambiaba de agente tenía que volver a contarle lo mismo: cuál es el host SSH de tal servidor, qué credenciales usar, cómo se despliega este proyecto, por qué la base está en aquel puerto y no en el otro. Una y otra vez. Le explicaba a Claude Code lo que ya le había explicado a OpenClaw ayer, y mañana se lo explicaría a Cursor. Yo era la memoria de mis agentes. Y no escalo.
Necesitaba —urgentemente— un sistema colaborativo donde el conocimiento viviera fuera del agente. Que el contexto fuera del proyecto, no de la herramienta. Así empezó la capa de memoria de ProjectHub. Este post es la historia de cómo, de tanto no querer repetirme, terminé con una memoria que mis agentes —y después mi equipo entero— consultan sin que yo diga una palabra.
El Problema No Era el Agente. Era Tener que Reentrenarlo.
Cuando trabajas con un solo asistente, su memoria interna basta. El archivo .md de turno, el contexto de la sesión. Pero en el momento en que tienes una flota —y cada agente vive en su propia herramienta, con su propio formato de memoria, en su propia máquina— el conocimiento queda atrapado en silos.
Lo que sé yo no lo sabe Claude Code. Lo que aprendió OpenClaw anoche no lo sabe Codex. Y el costo no es solo mío: cada repetición es una oportunidad de equivocarme, de darle a un agente una IP vieja o una credencial rotada. La fricción de reexplicar no es solo molesta: es una fuente de errores.
Así que me puse un objetivo simple de enunciar y difícil de cumplir: que el conocimiento sobreviva al agente. Que pueda cambiar de herramienta como quien cambia de editor, sin perder nada.
El Agente Dejó de Importar
La primera versión fue casi tonta de lo simple: sacar la memoria del agente y meterla en una base de datos común, accesible por todos vía una API y un servidor MCP. Cada recuerdo es una fila —AgentMemory— con estructura: un type (credential, config, fact, skill, note…), un label, tags, el content, y un workspace_id que lo aísla por proyecto.
El efecto fue inmediato y más profundo de lo que esperaba. De repente daba igual qué agente tuviera enfrente. Mi instrucción se redujo a una frase: "revisa las memorias de tal proyecto". Y listo. El agente cargaba el contexto —hosts, decisiones, credenciales, convenciones— y se ponía a trabajar como si llevara semanas en el proyecto.
# Al iniciar sesión, cualquier agente hace esto:
memory_search q="deploy y credenciales de projecthub"
# y deja de necesitarme como enciclopedia andanteEl agente se volvió intercambiable. Esa fue la primera victoria: el modelo —el que esté de moda este mes— pasó a ser el motor, no el dueño del conocimiento.
Enmascarado por Defecto, Revelado con Auditoría
Aquí hay que detenerse, porque una de las cosas que más repetía eran precisamente las credenciales. Y meter contraseñas en una memoria compartida suena a desastre anunciado. Una memoria que filtra secretos es peor que no tener memoria.
Por eso los secretos son ciudadanos especiales. Cuando una memoria se marca como is_sensitive, el sistema la censura en todos lados. Listarla o buscarla devuelve el valor enmascarado:
// memory_search sobre una credencial
{
"label": "GitHub PAT — andyeswong",
"type": "credential",
"content": "[sensitive — click Reveal to view]",
"value": { "token": "********", "user": "********" }
}El valor real solo sale por una puerta: una llamada explícita a memory_get con el ID. Y esa puerta tiene cámara: cada revelación emite un evento de auditoría —secret.revealed— con quién, cuándo y qué. Además, las operaciones sensibles viven bajo su propio rate-limit (30 por minuto): si algo empieza a vaciar la bóveda en masa, queda registrado y se frena.
El principio: el agente puede usar un secreto sin que el secreto ande suelto. Saber que existe, de qué es y dónde aplica cubre casi todo; el valor en claro se pide en el instante exacto en que se necesita, y deja huella.
Búsqueda que Entiende, No que Coincide
Que el conocimiento esté guardado no sirve si el agente no lo encuentra. Y la búsqueda por palabra clave es traicionera: "servidor de IA" no encuentra "GPU host"; "el deploy de prod" no encuentra "runbook projecthub00".
Por eso la búsqueda es por significado. Cada memoria, al guardarse, se convierte en un vector con un modelo de embeddings corriendo local en Ollama —mxbai-embed-large—. Buscar es generar el vector de tu pregunta y traer las memorias que apuntan en la misma dirección (similitud coseno). Pides "la base de datos de producción" y aparece la nota de Postgres aunque no compartan una sola palabra.
# Guardar cuesta una sola llamada
memory_store type=config \
label="Deploy prod — projecthub00" \
content="App Laravel en /home/andyeswong/agentProjectHub, php8.5-fpm..."
# Recuperar por significado, no por keyword
memory_search q="cómo despliego a producción" # -> la nota de arriba, ordenada por similitudDe Recordar a Saber-Hacer: las Memorias Skill
El segundo salto vino solo. Me di cuenta de que no solo quería que mis agentes recordaran datos (este host, aquella credencial). Quería que supieran hacer cosas. Que un procedimiento que descubrí una vez —con sus comandos exactos, sus trampas, su orden— no se perdiera ni tuviera que reconstruirlo.
Así nacieron las memorias de tipo skill: cápsulas con todo lo necesario para ejecutar una tarea concreta. Un mini-framework guardado. No "la IP del servidor X", sino "cómo correr DDL en el Postgres self-hosted: entra por SSH a tal workspace, el contenedor se llama así, ejecuta psql de esta forma, y acuérdate de recargar el cache de PostgREST y dar GRANT al rol anon".
memory_store type=skill \
label="Skill: correr DDL en Supabase self-hosted" \
content="SSH al workspace -> docker exec supabase-db psql -U postgres ...
GRANT ... TO anon; NOTIFY pgrst,'reload schema'; verificar con curl"La diferencia es enorme. Una memoria fact le dice al agente qué. Una memoria skill le dice cómo. Cuando un agente arranca una tarea, busca la skill correspondiente y ejecuta el procedimiento probado en vez de improvisar —y volver a tropezar con la misma trampa que yo ya documenté—.
El Salto Final: Memoria Colaborativa con el Equipo
Hasta aquí el beneficiado era yo. El tercer salto cambió cómo trabaja todo el equipo.
Si la memoria vive en una base común, org-scoped, ¿por qué limitarla a mis agentes? Empecé a compartir memorias con mi equipo de desarrollo. Y pasó algo que no había anticipado del todo: dejé de explicarles cosas. No porque dejara de importarme, sino porque ya no hacía falta. El agente de cada desarrollador consulta las memorias colaborativas y saca lo que necesita: las convenciones del proyecto, los hosts, los procedimientos, las decisiones de arquitectura que tomamos y por qué.
El onboarding dejó de ser una llamada de una hora repitiendo lo de siempre. Ahora es: "tu agente ya tiene acceso a las memorias del proyecto, pregúntale". El conocimiento que antes vivía en mi cabeza —y que yo dosificaba a mano, persona por persona— se volvió un recurso consultable. La memoria pasó de ser mi muleta a ser infraestructura del equipo.
El Día que un Tercio de mi Memoria era Invisible
Toda esta confianza tiene una condición: que la memoria de verdad devuelva lo que guardas. Y un día descubrí que no lo estaba haciendo.
Noté que ciertas memorias que yo sabía que existían no aparecían en las búsquedas. Al medir, el golpe: cerca del 34% de las memorias no tenían embedding. Guardadas, intactas en la base, pero invisibles a la búsqueda semántica. Y una memoria que no se puede recuperar, para efectos prácticos, no existe.
La causa estaba en los logs viejos. El servicio usaba el endpoint legacy POST /api/embeddings de Ollama, que en la versión nueva devuelve HTTP 500 —"input length exceeds the context length"— cuando el texto pasa los ~512 tokens del modelo. O sea: las memorias largas, las más valiosas, eran justo las que fallaban en silencio. Se guardaba el texto, el embedding quedaba nulo, y nadie se enteraba. Probé el endpoint nuevo /api/embed con truncate=true, pero ese build de Ollama ignora el flag: 1000 caracteres pasaban, 1400 fallaban, con flag o sin él.
El fix, tres piezas:
- Migrar a
/api/embedy parsear bien la respuesta. - Recorte duro a 1000 caracteres antes de enviar —
mxbai-embed-largesolo consume ~512 tokens igual; el resto no aportaba al vector, solo reventaba la petición—. - Un comando para reparar lo ya roto:
php artisan memory:reembed, que recorre las memorias con embedding nulo y las reprocesa leyendo la base directo, sin pasar por la API.
php artisan memory:reembed --dry-run # cuántas faltan
php artisan memory:reembed # repararlas in-processResultado: 287 de 287 re-embebidas, cero fallos. El aprendizaje no es sobre Ollama: un sistema de memoria necesita una forma de verificar que recuerda. Si el guardado puede fallar en silencio, tomas decisiones creyendo que el agente "ya sabe" algo que nunca pudo recuperar. Hoy mido cuántas memorias tienen embedding como quien mide uptime. (Detalle fino: con separación lectura/escritura, no verifiques el re-embed contra la respuesta inmediata del update —la réplica con lag te miente—; verifica contra una lectura consistente.)
Lo que Sigue: de Coseno en Memoria a pgvector
Hoy la búsqueda hace un full scan calculando coseno en código sobre unos cientos de vectores. Con ese volumen va perfecto y no optimizo lo que no duele. Pero no escala a miles. El plan, cuando los números lo pidan, es mover los vectores a Postgres con pgvector: columna vector(1024), índice HNSW y búsqueda por <=> en SQL en vez de en PHP. Lo digo porque también es una postura: no migras infraestructura por moda, la migras cuando el volumen lo exige.
El Código
ProjectHub es abierto. El backend —donde vive la capa de memoria, la API y el sistema de tareas— está en github.com/andyeswong/agentProjectHub (Laravel/PHP), y la superficie MCP que expone todo esto a cualquier agente está en github.com/andyeswong/projecthub-mcp (TypeScript). El comando memory:reembed, el enmascarado y la búsqueda por embeddings que describí viven en el primero.
Conclusión
Todo esto empezó por algo muy poco glamoroso: estaba harto de repetirme. De darle a cada agente el mismo host, la misma credencial, la misma explicación. La solución no fue un agente más listo —fue sacar el conocimiento del agente y ponerlo donde todos pudieran verlo: enmascarado cuando es secreto, buscable por significado, y empaquetado como skills cuando es un procedimiento.
El resultado se mide en una frase que hoy uso a diario y que hace un año habría sido impensable: "revisa las memorias". Da igual si del otro lado está Claude, Cursor, Codex u OpenClaw. Da igual si soy yo o un desarrollador de mi equipo. El modelo es el motor; la memoria —segura, compartida y recuperable— es el producto.
artículos_relacionados
El Modelo es Prescindible: Cómo Construí una Memoria por Embeddings que Sobrevive a Cualquier Agente
Cómo una actualización de OpenClaw que borró toda la memoria en .md me obligó a construir un sistema de memoria persistente con embeddings sobre Ollama, mxbai-embed y Redis, donde el contexto, los proyectos y la identidad del agente sobreviven a cualquier modelo, reinstalación o cambio de herramienta.
Embeddings y Por Qué Funciona RAG: Cómo un Modelo Responde Sobre lo que Nunca Vio
Le preguntas al LLM más caro del mundo sobre tus propios documentos y te responde con seguridad... una mentira. Este es el porqué, y la solución: qué es realmente un embedding, cómo la similitud coseno mide significado, y por qué RAG —recuperar y aumentar antes de generar— hace que un modelo responda sobre datos que nunca estuvieron en su entrenamiento, sin reentrenar nada.