Skip to content

Archivos de memoria

源码版本v2026.6.11

Responsabilidad

Los «archivos de memoria» (memory files) son las preferencias de usuario, guías de comportamiento y contexto de proyecto que el agent de OpenClaw persiste a largo plazo. No son lo mismo que un session transcript: una sesión es la lista mensaje a mensaje de una conversación, la memoria (memory) es un conjunto de notas «esta regla siempre aplica» que se reutiliza entre sesiones. OpenClaw reconoce por defecto MEMORY.md en la raíz del workspace como archivo raíz de memoria (root memory file), y todos los .md bajo el subdirectorio memory/ como memoria auxiliar; al arrancar el agent los lee al system prompt, y a partir de ahí toda la conversación sigue esas reglas.

El sistema de memoria también soporta multimodal: bajo memory/ no solo hay .md, también se pueden colocar imágenes, audio u otros archivos con extensiones permitidas por MemoryMultimodalSettings, que el memory-host-sdk clasifica e indexa.

Motivación de diseño

¿Por qué no escribir todas las reglas directamente en el system prompt? Tres razones:

  1. Reutilización entre sesiones: el system prompt consume tokens en cada llamada al modelo; los archivos de memoria solo se cargan una vez al arrancar el agent y se reutilizan en toda la sesión. Las preferencias a largo plazo son más baratas en archivo que en prompt.
  2. Editables por el usuario: los archivos de memoria son markdown normal; el usuario los edita con cualquier editor y la próxima vez que arranque surten efecto, sin tocar config ni reiniciar el gateway. openclaw doctor --fix además puede migrar archivos legacy y mergear coplias canonical/legacy divididas.
  3. Indexables por plugins: memory-host-sdk registra los archivos de memoria como QMD collections, los plugins pueden construir índices vectoriales y hacer retrieval semántico — así la memoria larga no se mete entera en el prompt, sino que se recalls dinámicamente por query.

El coste es que el nombre del archivo raíz tiene historia legacy, es case-sensitive, y los symlinks se ignoran — todos estos detalles los gestiona root-memory-files.ts.

Archivos clave

Flujo de datos

listMemoryFiles (listMemoryFiles:150) es la entrada central de escaneo:

typescript
export async function listMemoryFiles(
  workspaceDir: string,
  extraPaths?: string[],
  multimodal?: MemoryMultimodalSettings,
): Promise<string[]> {
  const result: string[] = [];
  const memoryDir = path.join(workspaceDir, "memory");

  const shouldSkipWorkspaceMemoryPath = (absPath: string): boolean =>
    shouldSkipRootMemoryAuxiliaryPath({ workspaceDir, absPath });

  const addMarkdownFile = async (absPath: string) => {
    try {
      const stat = await statRegularFile(absPath);
      if (stat.missing) {
        return;
      }
      if (!absPath.endsWith(".md")) {
        return;
      }
      result.push(absPath);
    } catch {}
  };

  const memoryFile = await resolveCanonicalRootMemoryFile(workspaceDir);
  if (memoryFile) {
    await addMarkdownFile(memoryFile);
  }
  try {
    const dirStat = await fs.lstat(memoryDir);
    if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
      await collectMemoryFilesFromDir(memoryDir, result, multimodal, shouldSkipWorkspaceMemoryPath);
    }
  } catch {}

Tres capas: el archivo canonical root, resolveCanonicalRootMemoryFile solo devuelve archivo real (no symlink), evitando que un soft link arrastre directorios externos; el directorio memory/ pasa por collectMemoryFilesFromDir recursivo, el hook descend rechaza el directorio .openclaw-repair y el hook include filtra extensiones con isAllowedMemoryFilePath; los extra paths vienen de la config, cada uno pasa por shouldSkipRootMemoryAuxiliaryPath para evitar contar duplicados legacy.

backend-config registra estos archivos como QMD collections (resolveDefaultCollections:409) para que los indexe el index vectorial:

typescript
function resolveDefaultCollections(
  include: boolean,
  workspaceDir: string,
  existing: Set<string>,
  agentId: string,
): ResolvedQmdCollection[] {
  if (!include) {
    return [];
  }
  const entries: Array<{ path: string; pattern: string; base: string }> = [
    { path: workspaceDir, pattern: CANONICAL_ROOT_MEMORY_FILENAME, base: "memory-root" },
    { path: path.join(workspaceDir, "memory"), pattern: "**/*.md", base: "memory-dir" },
  ];
  return entries.map((entry) => ({
    name: ensureUniqueName(scopeCollectionBase(entry.base, agentId), existing),
    path: entry.path,
    pattern: entry.pattern,
    kind: "memory",
  }));
}

memory-root hace match exacto de nombre, no recursivo; memory-dir es glob **/*.md, recursivo por todo el subárbol memory/. scopeCollectionBase prefija el agent id a cada collection name, así cuando varios agents comparten un workspace sus collections no colisionan.

La lógica de doctor para detectar archivos divididos (detectRootMemoryFiles:97) usa exactWorkspaceEntryExists para comprobar a la vez MEMORY.md y memory.md:

typescript
export async function detectRootMemoryFiles(
  workspaceDir: string,
): Promise<RootMemoryFilesDetection> {
  const resolvedWorkspace = path.resolve(workspaceDir);
  const canonicalPath = resolveCanonicalRootMemoryPath(resolvedWorkspace);
  const legacyPath = resolveLegacyRootMemoryPath(resolvedWorkspace);
  const entries = await listWorkspaceEntries(resolvedWorkspace);
  const [canonical, legacy] = await Promise.all([
    entries.has(CANONICAL_ROOT_MEMORY_FILENAME)
      ? statIfExists(canonicalPath)
      : Promise.resolve<RootMemoryStatResult>({ exists: false }),
    entries.has(LEGACY_ROOT_MEMORY_FILENAME)
      ? statIfExists(legacyPath)
      : Promise.resolve<RootMemoryStatResult>({ exists: false }),
  ]);

Notar que listWorkspaceEntries usa readdir para obtener entradas exactas (case-sensitive); macOS por defecto es case-insensitive a nivel filesystem, pero OpenClaw lo trata como case-sensitive: canonical siempre es MEMORY.md mayúscula, legacy es memory.md minúscula, y si ambos coexisten, canonical gana y legacy se archiva en .openclaw-repair/root-memory/<timestamp>/memory.md.

Cadena completa de lectura/escritura:

Límites y fallos

  • Symlinks ignorados: resolveCanonicalRootMemoryFile usa la doble comprobación entry.isFile() && !entry.isSymbolicLink() (src/memory/root-memory-files.ts:41-54), y el directorio memory/ también aplica !dirStat.isSymbolicLink(). Es un perímetro de seguridad, evita que el usuario (o un plugin malicioso) use soft links para arrastrar archivos externos al workspace al índice de memoria.
  • Distinción case: en filesystems case-insensitive (macOS HFS+/APFS por defecto), MEMORY.md y memory.md son el mismo archivo; pero OpenClaw usa exactWorkspaceEntryExists para obtener el nombre exacto de readdir y matchea literalmente. Esto significa que en macOS, si se escriben ambos se pisan, y doctor los trata como legacy-only.
  • Migración legacy no pierde contenido: moveLegacyRootMemoryFileToArchive primero rename, y si falla (EXDEV cross-device) cae a copy + unlink (src/commands/doctor-workspace.ts:152-174). El path de archive lleva timestamp, varias ejecuciones de doctor no se sobrescriben entre sí.
  • Extra paths cross-workspace: los extra paths declarados en config pueden ser absolutos o relativos al workspace, normalizeExtraMemoryPaths (packages/memory-host-sdk/src/host/internal.ts:91-101) expande tilde + deduplica, pero no los saca de la QMD collection — el plugin indexa por path absoluto y evita indexar el mismo archivo dos veces.
  • Path especial dreams.md: isMemoryPath también reconoce dreams.md como archivo de memoria (packages/memory-host-sdk/src/host/internal.ts:108-110), esta convención legacy se conserva para compatibilidad con usuarios antiguos. Canonical siempre es MEMORY.md, dreams.md es solo memoria auxiliar.
  • Extensiones multimodales: isAllowedMemoryFilePath por defecto solo acepta .md, pero cuando MemoryMultimodalSettings está activo permite extensiones de imagen/audio vía classifyMemoryMultimodalPath. Es un punto de extensión para plugins, core no trae indexación multimodal builtin.
  • readMemoryFile paginado: la lectura soporta parámetros from / lines (packages/memory-host-sdk/src/host/read-file.ts:67); defaultLines y maxChars los decide resolveAgentContextLimits según la config del agent, evitando que una lectura meta un archivo de memoria enorme entero al prompt.
  • Doctor detecta split: formatRootMemoryFilesWarning avisa cuando canonical y legacy coexisten (src/commands/doctor-workspace.ts:128-140): «Dreaming escribe promociones durables a MEMORY.md, los hechos viejos en legacy quedan shadowed». Es el aviso del conflicto entre el mecanismo builtin de «dreaming» (que promueve memoria a MEMORY.md automáticamente) y los archivos legacy.
  • Plugin SDK no importa core interno directamente: openclaw-runtime-memory.ts (packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts) solo re-exporta seams estables (resolveCanonicalRootMemoryFile / shouldSkipRootMemoryAuxiliaryPath / buildActiveMemoryPromptSection); el plugin llama a estas funciones vía SDK sin importar src/memory/ — así al refactorizar core los plugins no se rompen.
  • Attestation de workspace: el directorio workspace se attestea en el primer uso (src/agents/workspace.ts:44 WORKSPACE_ATTESTATION_DIRNAME), el attestation guarda inode/dev, evitando que el usuario cambie el filesystem a mitad de operación y los paths de memoria se desplazen.

Resumen

Los archivos de memoria son el layer de preferencias persistentes del agent entre sesiones. MEMORY.md es el archivo raíz canonical, memory/ es memoria auxiliar, y extra paths deja referenciar entre workspaces. memory-host-sdk se ocupa de escanear, registrar QMD collections y paginar la lectura, los plugins se conectan vía la facade del SDK, y el comando doctor gestiona migración legacy y reparación de splits. Cómo el context engine sincroniza la memoria al compactar en Context Engine; a qué sesión se carga la memoria en el ciclo de vida del Gestor de sesiones.

Referencias oficiales: Documentación de Memory · README.