From 37197b883d94cb21f6790762e490dcdae0a52410 Mon Sep 17 00:00:00 2001 From: Junebugg1214 <82672745+Junebugg1214@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:39:58 -0400 Subject: [PATCH] fix: preserve multimodal memory sync inputs --- src/memory/index.test.ts | 20 ++++++++++++++++ src/memory/manager-sync-ops.ts | 44 ++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index dcb0b061073..2defe9b1b61 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -315,6 +315,26 @@ describe("memory index", () => { expect(audioResults.some((result) => result.path.endsWith("meeting.wav"))).toBe(true); }); + it("indexes a multimodal extra path provided as a direct file path", async () => { + const mediaDir = path.join(workspaceDir, "media-single-file"); + const imagePath = path.join(mediaDir, "diagram.png"); + await fs.mkdir(mediaDir, { recursive: true }); + await fs.writeFile(imagePath, Buffer.from("png")); + + const cfg = createCfg({ + storePath: path.join(workspaceDir, `index-multimodal-file-${randomUUID()}.sqlite`), + provider: "gemini", + model: "gemini-embedding-2-preview", + extraPaths: [imagePath], + multimodal: { enabled: true, modalities: ["image"] }, + }); + const manager = await getPersistentManager(cfg); + await manager.sync({ reason: "test" }); + + const imageResults = await manager.search("image"); + expect(imageResults.some((result) => result.path.endsWith("diagram.png"))).toBe(true); + }); + it("skips oversized multimodal inputs without aborting sync", async () => { const mediaDir = path.join(workspaceDir, "media-oversize"); await fs.mkdir(mediaDir, { recursive: true }); diff --git a/src/memory/manager-sync-ops.ts b/src/memory/manager-sync-ops.ts index 776466adad5..520d567a852 100644 --- a/src/memory/manager-sync-ops.ts +++ b/src/memory/manager-sync-ops.ts @@ -3,7 +3,7 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; -import chokidar, { FSWatcher } from "chokidar"; +import chokidar from "chokidar"; import { resolveAgentDir } from "../agents/agent-scope.js"; import { ResolvedMemorySearchConfig } from "../agents/memory-search.js"; import { type OpenClawConfig } from "../config/config.js"; @@ -35,6 +35,11 @@ import { } from "./internal.js"; import { type MemoryFileEntry } from "./internal.js"; import { ensureMemoryIndexSchema } from "./memory-schema.js"; +import { + buildCaseInsensitiveExtensionGlob, + getMemoryMultimodalExtensions, + isMemoryMultimodalEnabled, +} from "./multimodal.js"; import type { SessionFileEntry } from "./session-files.js"; import { buildSessionEntry, @@ -62,6 +67,11 @@ type MemorySyncProgressState = { report: (update: MemorySyncProgressUpdate) => void; }; +type MemoryWatcher = { + on(event: "add" | "change" | "unlink", listener: () => void): unknown; + close(): Promise; +}; + const META_KEY = "memory_index_meta_v1"; const VECTOR_TABLE = "chunks_vec"; const FTS_TABLE = "chunks_fts"; @@ -121,7 +131,7 @@ export abstract class MemoryManagerSyncOps { loadError?: string; } = { enabled: false, available: false }; protected vectorReady: Promise | null = null; - protected watcher: FSWatcher | null = null; + protected watcher: MemoryWatcher | null = null; protected watchTimer: NodeJS.Timeout | null = null; protected sessionWatchTimer: NodeJS.Timeout | null = null; protected sessionUnsubscribe: (() => void) | null = null; @@ -384,9 +394,27 @@ export abstract class MemoryManagerSyncOps { } if (stat.isDirectory()) { watchPaths.add(path.join(entry, "**", "*.md")); + if (isMemoryMultimodalEnabled(this.settings.multimodal)) { + for (const modality of this.settings.multimodal.modalities) { + for (const extension of getMemoryMultimodalExtensions(modality)) { + watchPaths.add( + path.join(entry, "**", buildCaseInsensitiveExtensionGlob(extension)), + ); + } + } + } continue; } - if (stat.isFile() && entry.toLowerCase().endsWith(".md")) { + if ( + stat.isFile() && + (entry.toLowerCase().endsWith(".md") || + (isMemoryMultimodalEnabled(this.settings.multimodal) && + this.settings.multimodal.modalities.some((modality) => + getMemoryMultimodalExtensions(modality).some((extension) => + entry.toLowerCase().endsWith(extension), + ), + ))) + ) { watchPaths.add(entry); } } catch { @@ -650,9 +678,15 @@ export abstract class MemoryManagerSyncOps { return; } - const files = await listMemoryFiles(this.workspaceDir, this.settings.extraPaths); + const files = await listMemoryFiles( + this.workspaceDir, + this.settings.extraPaths, + this.settings.multimodal, + ); const fileEntries = ( - await Promise.all(files.map(async (file) => buildFileEntry(file, this.workspaceDir))) + await Promise.all( + files.map(async (file) => buildFileEntry(file, this.workspaceDir, this.settings.multimodal)), + ) ).filter((entry): entry is MemoryFileEntry => entry !== null); log.debug("memory sync: indexing memory files", { files: fileEntries.length,