diff --git a/src/cli/memory-cli.ts b/src/cli/memory-cli.ts index c4e47ec8d..16520a49d 100644 --- a/src/cli/memory-cli.ts +++ b/src/cli/memory-cli.ts @@ -266,7 +266,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { | Awaited> | undefined; let indexError: string | undefined; - const syncFn = manager.sync; + const syncFn = manager.sync ? manager.sync.bind(manager) : undefined; if (deep) { await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => { progress.setLabel("Probing vector…"); @@ -519,7 +519,7 @@ export function registerMemoryCli(program: Command) { }, run: async (manager) => { try { - const syncFn = manager.sync; + const syncFn = manager.sync ? manager.sync.bind(manager) : undefined; if (opts.verbose) { const status = manager.status(); const rich = isRich(); diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index 2fd2e2c1b..a465260b4 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -6,7 +6,7 @@ import { EventEmitter } from "node:events"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("node:child_process", () => { - const spawn = vi.fn((cmd: string, _args: string[]) => { + const spawn = vi.fn((_cmd: string, _args: string[]) => { const stdout = new EventEmitter(); const stderr = new EventEmitter(); const child = new EventEmitter() as { diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 87fe22729..433ddbcba 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -74,6 +74,8 @@ export class QmdMemoryManager implements MemorySearchManager { private readonly xdgCacheHome: string; private readonly collectionsFile: string; private readonly indexPath: string; + private readonly legacyCollectionsFile: string; + private readonly legacyIndexPath: string; private readonly env: NodeJS.ProcessEnv; private readonly collectionRoots = new Map(); private readonly sources = new Set(); @@ -107,6 +109,12 @@ export class QmdMemoryManager implements MemorySearchManager { this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache"); this.collectionsFile = path.join(this.xdgConfigHome, "qmd", "index.yml"); this.indexPath = path.join(this.xdgCacheHome, "qmd", "index.sqlite"); + + // Legacy locations (older builds wrote here). Keep them in sync via symlinks + // so upgrades don't strand an empty index. + this.legacyCollectionsFile = path.join(this.qmdDir, "config", "index.yml"); + this.legacyIndexPath = path.join(this.qmdDir, "cache", "index.sqlite"); + this.env = { ...process.env, XDG_CONFIG_HOME: this.xdgConfigHome, @@ -141,8 +149,13 @@ export class QmdMemoryManager implements MemorySearchManager { await fs.mkdir(path.dirname(this.collectionsFile), { recursive: true }); await fs.mkdir(path.dirname(this.indexPath), { recursive: true }); + // Legacy dirs + await fs.mkdir(path.dirname(this.legacyCollectionsFile), { recursive: true }); + await fs.mkdir(path.dirname(this.legacyIndexPath), { recursive: true }); + this.bootstrapCollections(); await this.writeCollectionsConfig(); + await this.ensureLegacyCompatSymlinks(); if (this.qmd.update.onBoot) { await this.runUpdate("boot", true); @@ -176,6 +189,42 @@ export class QmdMemoryManager implements MemorySearchManager { } const yaml = YAML.stringify({ collections }, { indent: 2, lineWidth: 0 }); await fs.writeFile(this.collectionsFile, yaml, "utf-8"); + + // Also write legacy path so older qmd homes remain usable. + await fs.writeFile(this.legacyCollectionsFile, yaml, "utf-8"); + } + + private async ensureLegacyCompatSymlinks(): Promise { + // Best-effort: keep legacy locations pointing at the XDG locations. + // This helps when users have old state dirs on disk. + try { + await fs.rm(this.legacyCollectionsFile, { force: true }); + } catch {} + try { + await fs.symlink(this.collectionsFile, this.legacyCollectionsFile); + } catch {} + + try { + // If a legacy index already exists (from an older version), prefer it by + // linking the XDG path to the legacy DB. + const legacyExists = await fs + .stat(this.legacyIndexPath) + .then(() => true) + .catch(() => false); + const xdgExists = await fs + .stat(this.indexPath) + .then(() => true) + .catch(() => false); + + if (legacyExists && !xdgExists) { + await fs.symlink(this.legacyIndexPath, this.indexPath); + } else if (!legacyExists && xdgExists) { + // nothing to do + } else if (!legacyExists && !xdgExists) { + // Create an empty file so the path exists for read-only opens later. + await fs.writeFile(this.indexPath, ""); + } + } catch {} } async search( diff --git a/src/memory/search-manager.test.ts b/src/memory/search-manager.test.ts index f7119caa1..8419d7f2c 100644 --- a/src/memory/search-manager.test.ts +++ b/src/memory/search-manager.test.ts @@ -59,6 +59,7 @@ describe("getMemorySearchManager caching", () => { const second = await getMemorySearchManager({ cfg, agentId: "main" }); expect(first.manager).toBe(second.manager); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(QmdMemoryManager.create).toHaveBeenCalledTimes(1); }); }); diff --git a/src/memory/search-manager.ts b/src/memory/search-manager.ts index 5d98f6451..afd392673 100644 --- a/src/memory/search-manager.ts +++ b/src/memory/search-manager.ts @@ -2,7 +2,6 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import type { MoltbotConfig } from "../config/config.js"; import { resolveMemoryBackendConfig } from "./backend-config.js"; import type { ResolvedQmdConfig } from "./backend-config.js"; -import type { MemoryIndexManager } from "./manager.js"; import type { MemoryEmbeddingProbeResult, MemorySearchManager,