Memoria y sincronización
La memoria es un plugin de arquitectura integrado de oHarness (memory), no un servidor MCP. Registra las herramientas de memoria, el índice del prompt de sistema, la recuperación por mensaje, la extracción opcional, la página Memory y un sincronizador en segundo plano, todo a través del mismo PluginHost que los plugins de modelos, herramientas y auditoría. La extensión de página se mantiene pequeña: descriptores registrados más renderizadores propiedad del host (actualmente memory), no JavaScript arbitrario en la interfaz ni un SDK de iframe.
Dónde viven las notas
Una nota por archivo Markdown, con frontmatter JSON:
---
{"id":"pricing-borderless-3fa","summary":"Pricing cards use fills, not borders","type":"feedback","tags":["design"],"createdAt":1789000000000,"updatedAt":1789000000000}
---
Cards on the pricing page are separated by fill and whitespace.
**Why:** the outlines were the loudest thing on a page of numbers.
**How to apply:** keep the border declared but transparent. See [[pricing-layout-a91]].
| Ámbito | Directorio | Contiene |
|---|---|---|
project | <project>/MEMORY/ | Decisiones, restricciones y referencias que se mueven y se exportan con el proyecto |
user | ~/.oharness/memory/user/ | Quién es el usuario y cómo le gusta trabajar, en todos los proyectos |
Cada proyecto registrado lleva .oharness/project.json. Su id estable mantiene vinculadas las sesiones del lado Home, el estado de sincronización y las estadísticas de recuperación cuando se renombra o se exporta la carpeta. Un repositorio git usa inicialmente su commit raíz como id; los demás directorios reciben un id de proyecto aleatorio. OHARNESS_HOME traslada las notas de usuario y el estado local de la máquina, pero nunca reubica el directorio MEMORY/ del proyecto.
memory.dir (o el antiguo memoryDir) en la configuración anula el directorio del proyecto, relativo a la raíz del proyecto. Si no se puede crear el directorio de usuario, las notas de proyecto siguen disponibles en <project>/MEMORY; solo dejan de estar disponibles las notas de usuario y el estado de memoria del lado Home, y un aviso lo indica.
Tipos
| Tipo | Ámbito predeterminado | Qué registra |
|---|---|---|
user | user | El rol, la experiencia y las preferencias de trabajo del usuario |
feedback | project | Una corrección, o un enfoque que el usuario confirmó |
project | project | Una decisión y su motivo, una restricción, trabajo en curso que no se deduce del código |
reference | project | Dónde está la información externa |
Solo las notas user van por defecto al ámbito de usuario. El feedback va por defecto al proyecto, porque la mayoría de las correcciones tratan de cómo se trabaja en un código concreto; una que claramente vale en todas partes se pone a propósito en el ámbito de usuario.
Las notas escritas antes de que existieran los tipos no tienen ninguno y siguen funcionando. type forma parte del hash de contenido de una nota solo cuando está presente, así que las notas existentes conservan los hashes que ya tienen sus bases de sincronización.
Escribir notas
El modelo escribe con remember, lee con memory y borra con forget. remember del plugin de memoria pasa sin preguntar en todos los modos de permisos excepto plan, que lo rechaza como cualquier otra escritura. Una regla ask o deny que nombre remember sigue aplicándose, forget sigue preguntando, y la herramienta remember de otro plugin no recibe esa exención. Las etiquetas se recortan, se separan por comas y espacios y se deduplican antes de guardar la nota. La sección Memory del prompt de sistema indica cuándo merece la pena escribir una nota y cuándo no. remember devuelve las notas existentes más relacionadas con lo que se acaba de escribir, señala un probable duplicado y enumera los enlaces que apuntan a notas que aún no existen. Se rechaza todo lo que parezca una credencial. Se pide que las notas lleven etiquetas con las otras palabras que alguien podría usar: sinónimos, y el término en el otro idioma en uso, ya que la recuperación compara palabras y no significados.
Las notas se enlazan entre sí en el cuerpo como [[note-id]]. Los enlaces se derivan de los cuerpos cada vez que se cargan las notas; no se guarda nada de ellos por separado, y el protocolo de sincronización no sabe que existen. Un enlace se resuelve primero en el ámbito del proyecto y luego en el de usuario. Un enlace a una nota inexistente se conserva: normalmente nombra una nota que alguien quería escribir.
Extracción automática
Desactivada por defecto, porque cada pasada es una llamada al modelo con el crédito del propio usuario. Con memory.autoExtract.onStop, al terminar una ejecución se entrega al modelo pequeño la conversación desde la última pasada, siempre que se hayan acumulado desde entonces al menos minUserTurns mensajes reales del usuario. Con onCompaction (u onStop), la parte del tramo que la compactación está a punto de descartar y que ninguna pasada ha leído aún se entrega antes, por corta que sea, ya que es la última oportunidad de leerla. Una sesión reanudada empieza a contar desde donde se reanudó, en lugar de volver a extraer todo su historial. Nunca se extrae de los subagentes.
Al extractor se le muestran las notas existentes más relacionadas con la conversación. Las pocas más relevantes llegan con su cuerpo completo, dentro de un presupuesto de tamaño, y solo esas pueden actualizarse; el resto se enumeran por su resumen para enlazarlas. Una actualización sustituye el cuerpo, así que antes se hace una copia de la versión sustituida en ~/.oharness/memory-backups/. Su respuesta se trata como no fiable: se descartan las actualizaciones a ids que no se le mostraron, los enlaces a ids que no se le mostraron pierden los corchetes, se ocultan las credenciales, y las notas nuevas de un mismo lote se refieren entre sí como [[new:N]] hasta que tienen id. La configuración de un repositorio puede desactivar la extracción; solo la configuración propia del usuario puede activarla.
La extracción termina después de que haya acabado la ejecución que la desencadenó, así que las notas que guardó se anuncian al principio del siguiente mensaje: «saved N memories from this conversation», en todas las interfaces.
Recuperación
Dos vías ponen notas delante del modelo:
- El índice. Al inicio de cada ejecución, una línea por nota, primero las actualizadas más recientemente, hasta
memory.indexCharscaracteres. El resto se cuentan, no se pierden; la herramientamemorybusca en todas. - Recuperación por mensaje. Cada mensaje del usuario se puntúa contra todas las notas, y las mejores coincidencias se adjuntan a ese mensaje dentro de
<recalled-memory>, con las notas que enlazan enumeradas por su resumen. Las interfaces muestran «recalled N memories». No se adjunta nada cuando nada supera el umbral, que es lo habitual. Si no hay ninguna nota, el índice se sustituye por dos líneas que dicen cuándo escribir una, en lugar de la guía completa.
La puntuación es léxica y se ejecuta en local: las palabras se reducen a su raíz, el texto CJK se divide en pares solapados, y las coincidencias en el resumen y las etiquetas pesan más que las del cuerpo. Una nota debe coincidir en su resumen o sus etiquetas para adjuntarse. Una nota adjuntada una vez no se vuelve a adjuntar mientras siga en la transcripción un mensaje que la lleve, incluso después de reanudar la sesión o reiniciar la app; los ids se leen de los metadatos de los mensajes. La compactación libera las notas cuyos mensajes resumió. Las notas adjuntadas van en su propio bloque del mensaje del usuario, registrado en los metadatos del mensaje, así que las transcripciones, los títulos de sesión y el historial web muestran las palabras del propio usuario. Los subagentes no recuperan notas.
Un fallo de la recuperación nunca impide enviar un mensaje.
Límites
| Ajuste | Predeterminado | Significado |
|---|---|---|
memory.maxNotes | 5000 | Por ámbito. Por encima, los archivos modificados hace más tiempo no se cargan y un aviso indica cuántos; las notas nuevas se rechazan hasta que se eliminen algunas |
memory.maxNoteBytes | 131072 | Tamaño del cuerpo; el servidor de sincronización aplica también su propio límite |
memory.indexChars | 4000 | Caracteres de resúmenes en el prompt de sistema |
memory.recall.enabled | true | Recuperación por mensaje |
memory.recall.perTurn | 3 | Notas adjuntadas a un mensaje |
memory.recall.budgetChars | 2000 | Caracteres que pueden ocupar esas notas |
memory.recall.minScore | 1.2 | Umbral para adjuntar |
memory.autoExtract.onCompaction / onStop | false | Extracción automática |
memory.autoExtract.minUserTurns | 3 | Las conversaciones más cortas se omiten |
Cuántas veces se ha recuperado cada nota se guarda por máquina en ~/.oharness/memory-usage/, fuera de las notas, así que la recuperación nunca provoca una subida de sincronización. Influye ligeramente en la clasificación y nunca se sincroniza.
Sincronización en la nube
- Las notas siempre funcionan en local, sin sesión iniciada y sin conexión.
- Iniciar sesión activa la sincronización automática por defecto. La sincronización va en ambos sentidos: los cambios locales suben, y los cambios hechos en otros dispositivos bajan solos.
- La página Memory ofrece 关闭云同步 y 立即同步. El interruptor abarca ambos ámbitos. La desactivación persiste tras reinicios y nuevos inicios de sesión, y bloquea las solicitudes a la nube automáticas y manuales hasta que se vuelva a activar explícitamente. Las herramientas locales y la edición siguen funcionando.
- Desactivar la sincronización cancela las solicitudes en curso. Los datos que Cloud ya aceptó no se pueden retirar; desactivar la sincronización no borra las copias en la nube.
- Cada ámbito lógico de proyecto o usuario queda fijado a la primera cuenta y al primer origen de Cloud con los que se sincronizó. Cambiar de cuenta o de origen pausa la sincronización. La página no ofrece ninguna reasignación insegura con un solo clic.
La página mantiene ~/.oharness/plugins.json (OHARNESS_HOME lo anula), en runtimePlugins[id=memory].config.syncTargets[sha256(stableScopeKey)]:
{ "userId": "verified-account-label", "origin": "https://your-cloud.example", "enabled": false }
No pongas claves de R2 ni configuración de Supabase en este archivo. La URL de Cloud sigue en cloud.json; las credenciales de R2 solo existen en el entorno del despliegue de sincronización. Para desactivar el plugin entero (página, herramientas, prompts, recuperación y trabajo en segundo plano), añade { "id": "memory", "enabled": false } a runtimePlugins; surte efecto en la siguiente creación del Harness. El registro de proyectos sigue creando los directorios estándar vacíos MEMORY/ y ARTIFACTS/. Para quien lo integra, harness.memory es el almacén del proyecto y harness.memoryUserService el del usuario; ambos están ausentes cuando el plugin está desactivado. AgentOptions.memory sigue siendo una API de integración de más bajo nivel y obtiene recuperación sin un host de plugins.
Cuándo se sincroniza
Un único coordinador por proceso programa todos los directorios de notas, independientemente de cuántos proyectos tengan abiertos un servidor web local o la aplicación de escritorio. Cada directorio tiene un solo servicio de sincronización en el proceso, así que las notas propias del usuario se sincronizan una vez y no una vez por cada proyecto abierto.
- Una escritura local se sube unos tres segundos después; una ráfaga de escrituras es una sola pasada.
- Si no, cada directorio se comprueba cada minuto, espaciándose hasta cada cinco minutos mientras no cambie nada en ningún lado.
- Tus propias notas siempre se comprueban. Las notas de un proyecto solo se comprueban mientras el proyecto está en uso (se envía un mensaje en él o está abierta la página Memory) y durante los treinta minutos siguientes.
- El proceso se limita a unas veinte solicitudes por minuto, dejando margen bajo el límite por cuenta del servicio de sincronización para otros dispositivos.
- Un
429pausa todos los directorios hasta la hora indicada en suRetry-After, y la página dice que la sincronización fue demasiado frecuente y que se reintentará, en lugar de culpar a la configuración.
立即同步 siempre se ejecuta en el acto.
Qué muestra la página
El interruptor, «last synced» y los errores abarcan ambos ámbitos: la sincronización cuenta como activada solo cuando lo está en los dos, la hora mostrada es la más antigua de ambas, y cada error indica si se refiere a las notas del proyecto o a las tuyas. La página muestra también lo que trajo la última descarga (añadidas, cambiadas, eliminadas) y, en un plan gratuito, los límites del plan.
Ordenar las notas
La lista 待整理 de la página es una revisión que se ejecuta en esta máquina, sin modelo:
| Hallazgo | Significado | Qué puedes hacer |
|---|---|---|
| Mismo tema | De dos a seis notas de un mismo ámbito cuyos resúmenes y etiquetas se solapan en gran parte | Fusionar o descartar |
| Enlace roto | Un [[id]] que no nombra ninguna nota | Abrir la nota o descartar |
| Archivo ausente | Una nota de proyecto nombra una ruta relativa que no está en el proyecto | Abrir o descartar |
| Sin uso | Ni recuperada ni editada durante memory.consolidate.unusedDays (90) | Abrir o descartar |
| Demasiado larga | Un cuerpo de más de 8.000 caracteres | Abrir o descartar |
Un descarte se guarda en local asociado a las versiones actuales de las notas, así que editar una de ellas hace que el hallazgo vuelva.
Fusionar abre un editor con un borrador: el del modelo pequeño (生成合并建议) o todos los originales unidos (手动合并). Al aplicarlo se conserva el id de la nota más antigua, se borran las demás, todos los enlaces [[deleted]] de ese ámbito pasan a apuntar a la nota conservada y se hace copia de seguridad de cada nota que cambia. Se rechaza una fusión planificada a partir de notas que han cambiado desde entonces.
Con la sincronización en la nube activada, primero se sincroniza el ámbito y la fusión se confirma en la nube antes que en esta máquina (commitFirst), así que una edición desde otro dispositivo entre medias rechaza la fusión en lugar de convertirse en un conflicto. El cuerpo fusionado debe caber en el límite por nota del servicio de sincronización. Con la sincronización desactivada, la fusión es una edición local normal.
La fusión en segundo plano está activada por defecto solo en los planes de pago ("auto": "subscribers"): se ejecuta cuando la última respuesta del servicio de sincronización indicó que la cuenta, o cualquier equipo al que pertenece, tiene un plan de pago, y se mantiene desactivada en las cuentas gratuitas y siempre que el plan sea desconocido (sincronización en la nube desactivada, sin sesión iniciada). true o false anula ese comportamiento:
{ "memory": { "consolidate": { "auto": false, "cooldownHours": 24, "maxMerges": 3 } } }
Cuando está activada, una pasada empieza dos minutos después del arranque como máximo una vez por periodo de espera y fusiona hasta maxMerges grupos de notas del mismo tema con el modelo pequeño, por la misma vía que la página. Nunca actúa sobre los demás hallazgos. Una configuración de proyecto puede desactivarla, pero no activarla.
Copias de seguridad y restauración
Se escribe una copia en ~/.oharness/memory-backups/ antes de sustituir o eliminar cualquier nota sin que alguien haya elegido esa versión: borrar en la página, forget, quedarse con la versión de la nube en un conflicto, una descarga automática que cambia o elimina una nota, una reescritura del extractor, y una restauración que sustituye una versión más reciente. La página Memory enumera las cincuenta más recientes con cuándo y por qué, y restaura cualquiera de ellas en el directorio del que procede; después la nota se sincroniza como cualquier otro cambio local. Las copias de seguridad escritas antes de que se registraran los motivos también se enumeran y pueden restaurarse.
Almacenamiento y conciliación
El servidor de sincronización expone /v1/memory/identity, /snapshot y /commit con autenticación. No guarda filas de base de datos. R2 contiene revisiones inmutables en Markdown más un manifiesto JSON que se sustituye de forma atómica:
users/<server-verified-user-id>/projects/<project-id>/memory/
users/<server-verified-user-id>/memory/ user scope
manifest.json
objects/<note-id>/<revision-id>.md
Las implementaciones que usen otro sistema de identidad o almacenamiento pueden hacerlo; las formas exactas de las solicitudes, el algoritmo de hash, el comportamiento CAS y los códigos de error se especifican en Protocolo de sincronización.
La base local (~/.oharness/memory-sync/) permite una conciliación a tres bandas: las notas sin cambios se omiten y los cambios en un solo lado se copian. Junto a ella el cliente guarda una copia de cada nota tal como se sincronizó por última vez, así que una nota cambiada en ambos lados se fusiona en lugar de solo marcarse:
- el resumen y el tipo vienen del lado que los cambió;
- las etiquetas se combinan como conjuntos, conservando lo que añadió cualquiera de los lados y quitando lo que eliminó cualquiera de ellos;
- los cuerpos se fusionan por líneas, y las ediciones separadas se combinan;
- una edición frente a un borrado conserva la edición, ya que volver a borrar es un clic y una edición perdida desaparece.
Se hace copia de seguridad de ambas versiones antes de una fusión. Lo que no se puede combinar (dos resúmenes o tipos distintos, ediciones en las mismas líneas o en líneas contiguas, una fusión demasiado grande para el plan, o una nota sin copia de su última versión sincronizada) se convierte en un conflicto. Un conflicto solo retiene su propia nota: todo lo demás se sigue subiendo y descargando. La página Memory muestra la última versión sincronizada, la de esta máquina y la de la nube, y ofrece conservar la de esta máquina, quedarse con la de la nube, conservar ambas (la de la nube pasa a ser una nota nueva al lado) o fusionar a mano; lo que se deje de lado se copia, venga del lado que venga. No hay sobrescritura del tipo «gana la última escritura». Los borrados son lápidas (tombstones), y las escrituras del manifiesto usan compare-and-swap con ETag.
Los cambios por lotes que nadie revisa nota a nota (la consolidación) se escriben primero en la nube: todas las notas implicadas deben estar totalmente sincronizadas, el commit lleva la versión del manifiesto que se comprobó, y solo cuando el servicio lo acepta se escribe el lote en local. Un cambio desde otro dispositivo entre medias lo rechaza el servicio en lugar de convertirse en un conflicto.
Las solicitudes enumeran los hashes de las notas que tiene esta máquina (have); el servidor ni lee ni devuelve esos cuerpos, así que una sincronización con pocos cambios transfiere poco. Los commits se dividen según la cuota de cambios que anuncia el servidor. Las lápidas más antiguas que la ventana del servidor se eliminan del manifiesto; una máquina que vuelve después de eso y encuentra que una nota que vio sincronizada por última vez, sin cambios, ya no figura, la borra, mientras que una nota que editó entretanto se convierte en conflicto. Se rechazan dos ids de nota que solo difieren en mayúsculas y minúsculas, ya que los sistemas de archivos de macOS y Windows no pueden contener ambos.
La sincronización reintenta los fallos transitorios tras una espera de 60 segundos; la sincronización manual puede reintentar de inmediato. Los conflictos esperan al usuario. La página guarda copia de las notas borradas, y de las versiones locales que pierden en los conflictos, en ~/.oharness/memory-backups/; forget hace lo mismo. Las ediciones directas en el sistema de archivos siguen admitiéndose, pero una escritura externa simultánea durante una fusión no está protegida por el bloqueo de escritura local del proceso; mantén un solo proceso en ejecución por directorio.
R2 es privado y usa TLS, pero esto no es cifrado de extremo a extremo: Cloud puede leer el contenido de las notas. Solo se sincronizan las notas, no los registros de sesión, los archivos ni las credenciales.
Cuotas y planes
El servidor es dueño de sus cuotas y las anuncia en cada respuesta; los clientes no guardan ninguna copia. Valores predeterminados: 5000 notas activas por proyecto y por usuario, 500 cambios por commit, 128 KiB por cuerpo, 16 MiB de cuerpos por respuesta, 90 días por lápida. Una cuota reducida solo bloquea el crecimiento, así que un usuario que la supere siempre puede borrar. Consulta Ejecutar tu propio servidor de sincronización para ver las variables.
Las cuentas gratuitas tienen además límites más estrictos: un número de notas por proyecto y por usuario, y un tamaño por nota, definidos en el src/memory-entitlements.json del servicio de sincronización. Las cuentas de pago no tienen límite de plan, solo la cuota anterior. El plan se lee del claim ent que Cloud escribe en el token de acceso: un plan de pago propio del usuario, o de cualquier equipo del que forme parte, cuenta como de pago. Solo se limita la sincronización en la nube; las notas en la máquina no, y una cuenta gratuita que supere el límite conserva todas sus notas locales y deja de subir las nuevas, con un mensaje que explica por qué.
Eliminar una cuenta
Eliminar una cuenta en el sitio web borra todo lo que sincronizó: los feeds de sesiones y la memoria de ambos ámbitos. La pasarela llama a POST /v1/admin/erase del servicio de sincronización con una clave que solo ella tiene (OHARNESS_SYNC_ADMIN_KEY en ambos), antes de eliminar la propia cuenta y una vez más después. El servicio de sincronización escribe primero una marca para la cuenta y, a partir de entonces, rechaza sus solicitudes de feed y de memoria, así que un dispositivo que siga con la sesión iniciada no puede volver a subir los datos. Las notas y sesiones de tus propias máquinas no se tocan.
Eliminar un equipo hace lo mismo con el historial de sesiones compartido del equipo: la pasarela borra feed/team-<id>/ (el borrado {team} del servicio de sincronización) después de cancelar la suscripción del equipo y antes de eliminar sus filas, y luego una vez más. A partir de entonces el servicio de sincronización rechaza los feeds de ese equipo, incluso para un token que todavía lo incluya. Los equipos no tienen memoria.
Actualizar
Actualiza el servidor de sincronización antes que los clientes. Un cliente nuevo recurre al protocolo antiguo contra un servidor antiguo para las notas de proyecto que no tienen tipo. No puede sincronizar notas de usuario, ni notas con tipo, con un servidor antiguo. Un cliente antiguo que recibe de la nube una nota con tipo deja de sincronizar con un error de suma de comprobación hasta que se actualiza.
Verificación y despliegue
Ejecuta npm run test:memory, npm run test:sync-memory, npm run test:plugins, npm run typecheck y npm run build. script/memory-ui-demo.ts inicia una UI aislada sin sesión en el puerto 4319 con un directorio personal temporal; nunca lee las credenciales del usuario. Si falta la configuración de R2 se produce un error de sincronización visible sin romper la memoria local. Reinicia el servidor de desarrollo local después de cambiar el código fuente.