fix(memory-qmd): create collections via qmd CLI (no YAML)
parent
dd8373a424
commit
564fe6f089
|
|
@ -78,17 +78,19 @@ describe("QmdMemoryManager", () => {
|
||||||
expect(manager).toBeTruthy();
|
expect(manager).toBeTruthy();
|
||||||
if (!manager) throw new Error("manager missing");
|
if (!manager) throw new Error("manager missing");
|
||||||
|
|
||||||
|
const baselineCalls = spawnMock.mock.calls.length;
|
||||||
|
|
||||||
await manager.sync({ reason: "manual" });
|
await manager.sync({ reason: "manual" });
|
||||||
expect(spawnMock.mock.calls.length).toBe(2);
|
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
|
||||||
|
|
||||||
await manager.sync({ reason: "manual-again" });
|
await manager.sync({ reason: "manual-again" });
|
||||||
expect(spawnMock.mock.calls.length).toBe(2);
|
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
|
||||||
|
|
||||||
(manager as unknown as { lastUpdateAt: number | null }).lastUpdateAt =
|
(manager as unknown as { lastUpdateAt: number | null }).lastUpdateAt =
|
||||||
Date.now() - (resolved.qmd?.update.debounceMs ?? 0) - 10;
|
Date.now() - (resolved.qmd?.update.debounceMs ?? 0) - 10;
|
||||||
|
|
||||||
await manager.sync({ reason: "after-wait" });
|
await manager.sync({ reason: "after-wait" });
|
||||||
expect(spawnMock.mock.calls.length).toBe(4);
|
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 4);
|
||||||
|
|
||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import YAML from "yaml";
|
|
||||||
|
|
||||||
import type { MoltbotConfig } from "../config/config.js";
|
import type { MoltbotConfig } from "../config/config.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||||
|
|
@ -72,10 +70,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||||
private readonly qmdDir: string;
|
private readonly qmdDir: string;
|
||||||
private readonly xdgConfigHome: string;
|
private readonly xdgConfigHome: string;
|
||||||
private readonly xdgCacheHome: string;
|
private readonly xdgCacheHome: string;
|
||||||
private readonly collectionsFile: string;
|
|
||||||
private readonly indexPath: string;
|
private readonly indexPath: string;
|
||||||
private readonly legacyCollectionsFile: string;
|
|
||||||
private readonly legacyIndexPath: string;
|
|
||||||
private readonly env: NodeJS.ProcessEnv;
|
private readonly env: NodeJS.ProcessEnv;
|
||||||
private readonly collectionRoots = new Map<string, CollectionRoot>();
|
private readonly collectionRoots = new Map<string, CollectionRoot>();
|
||||||
private readonly sources = new Set<MemorySource>();
|
private readonly sources = new Set<MemorySource>();
|
||||||
|
|
@ -102,19 +97,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||||
this.stateDir = resolveStateDir(process.env, os.homedir);
|
this.stateDir = resolveStateDir(process.env, os.homedir);
|
||||||
this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
|
this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
|
||||||
this.qmdDir = path.join(this.agentStateDir, "qmd");
|
this.qmdDir = path.join(this.agentStateDir, "qmd");
|
||||||
// QMD respects XDG base dirs:
|
// QMD uses XDG base dirs for its internal state.
|
||||||
// - config: $XDG_CONFIG_HOME/qmd/index.yml
|
// Collections are managed via `qmd collection add` and stored inside the index DB.
|
||||||
|
// - config: $XDG_CONFIG_HOME (contexts, etc.)
|
||||||
// - cache: $XDG_CACHE_HOME/qmd/index.sqlite
|
// - cache: $XDG_CACHE_HOME/qmd/index.sqlite
|
||||||
this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
|
this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
|
||||||
this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache");
|
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");
|
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 = {
|
this.env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
XDG_CONFIG_HOME: this.xdgConfigHome,
|
XDG_CONFIG_HOME: this.xdgConfigHome,
|
||||||
|
|
@ -146,16 +136,10 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||||
private async initialize(): Promise<void> {
|
private async initialize(): Promise<void> {
|
||||||
await fs.mkdir(this.xdgConfigHome, { recursive: true });
|
await fs.mkdir(this.xdgConfigHome, { recursive: true });
|
||||||
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
||||||
await fs.mkdir(path.dirname(this.collectionsFile), { recursive: true });
|
|
||||||
await fs.mkdir(path.dirname(this.indexPath), { 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();
|
this.bootstrapCollections();
|
||||||
await this.writeCollectionsConfig();
|
await this.ensureCollections();
|
||||||
await this.ensureLegacyCompatSymlinks();
|
|
||||||
|
|
||||||
if (this.qmd.update.onBoot) {
|
if (this.qmd.update.onBoot) {
|
||||||
await this.runUpdate("boot", true);
|
await this.runUpdate("boot", true);
|
||||||
|
|
@ -179,52 +163,28 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async writeCollectionsConfig(): Promise<void> {
|
private async ensureCollections(): Promise<void> {
|
||||||
const collections: Record<string, { path: string; pattern: string }> = {};
|
// QMD collections are persisted inside the index database and must be created
|
||||||
|
// via the CLI. The YAML file format is not supported by the QMD builds we
|
||||||
|
// target, so we ensure collections exist by running `qmd collection add`.
|
||||||
for (const collection of this.qmd.collections) {
|
for (const collection of this.qmd.collections) {
|
||||||
collections[collection.name] = {
|
try {
|
||||||
path: collection.path,
|
await this.runQmd([
|
||||||
pattern: collection.pattern,
|
"collection",
|
||||||
};
|
"add",
|
||||||
}
|
collection.path,
|
||||||
const yaml = YAML.stringify({ collections }, { indent: 2, lineWidth: 0 });
|
"--name",
|
||||||
await fs.writeFile(this.collectionsFile, yaml, "utf-8");
|
collection.name,
|
||||||
|
"--mask",
|
||||||
// Also write legacy path so older qmd homes remain usable.
|
collection.pattern,
|
||||||
await fs.writeFile(this.legacyCollectionsFile, yaml, "utf-8");
|
]);
|
||||||
}
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
private async ensureLegacyCompatSymlinks(): Promise<void> {
|
// Idempotency: qmd exits non-zero if the collection name already exists.
|
||||||
// Best-effort: keep legacy locations pointing at the XDG locations.
|
if (message.includes("already exists")) continue;
|
||||||
// This helps when users have old state dirs on disk.
|
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
||||||
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(
|
async search(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue