mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 02:18:25 +00:00
Memory/QMD: reuse default model cache and skip ENOENT warnings (#12114)
* Memory/QMD: symlink default model cache into custom XDG_CACHE_HOME QmdMemoryManager overrides XDG_CACHE_HOME to isolate the qmd index per-agent, but this also moves where qmd looks for its ML models (~2.1GB). Since models are installed at the default location (~/.cache/qmd/models/), every qmd invocation would attempt to re-download them from HuggingFace and time out. Fix: on initialization, symlink ~/.cache/qmd/models/ into the custom XDG_CACHE_HOME path so the index stays isolated per-agent while the shared models are reused. The symlink is only created when the default models directory exists and the target path does not already exist. Includes tests for the three key scenarios: symlink creation, existing directory preservation, and graceful skip when no default models exist. * Memory/QMD: skip model symlink warning on ENOENT * test: stabilize warning-filter visibility assertion (#12114) (thanks @tyler6204) * fix: add changelog entry for QMD cache reuse (#12114) (thanks @tyler6204) * fix: handle plain context-overflow strings in compaction detection (#12114) (thanks @tyler6204)
This commit is contained in:
@@ -604,6 +604,89 @@ describe("QmdMemoryManager", () => {
|
||||
).rejects.toThrow("qmd index busy while reading results");
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
describe("model cache symlink", () => {
|
||||
let defaultModelsDir: string;
|
||||
let customModelsDir: string;
|
||||
let savedXdgCacheHome: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Redirect XDG_CACHE_HOME so symlinkSharedModels finds our fake models
|
||||
// directory instead of the real ~/.cache.
|
||||
savedXdgCacheHome = process.env.XDG_CACHE_HOME;
|
||||
const fakeCacheHome = path.join(tmpRoot, "fake-cache");
|
||||
process.env.XDG_CACHE_HOME = fakeCacheHome;
|
||||
|
||||
defaultModelsDir = path.join(fakeCacheHome, "qmd", "models");
|
||||
await fs.mkdir(defaultModelsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(defaultModelsDir, "model.bin"), "fake-model");
|
||||
|
||||
customModelsDir = path.join(stateDir, "agents", agentId, "qmd", "xdg-cache", "qmd", "models");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedXdgCacheHome === undefined) {
|
||||
delete process.env.XDG_CACHE_HOME;
|
||||
} else {
|
||||
process.env.XDG_CACHE_HOME = savedXdgCacheHome;
|
||||
}
|
||||
});
|
||||
|
||||
it("symlinks default model cache into custom XDG_CACHE_HOME on first run", async () => {
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
|
||||
const stat = await fs.lstat(customModelsDir);
|
||||
expect(stat.isSymbolicLink()).toBe(true);
|
||||
const target = await fs.readlink(customModelsDir);
|
||||
expect(target).toBe(defaultModelsDir);
|
||||
|
||||
// Models are accessible through the symlink.
|
||||
const content = await fs.readFile(path.join(customModelsDir, "model.bin"), "utf-8");
|
||||
expect(content).toBe("fake-model");
|
||||
|
||||
await manager!.close();
|
||||
});
|
||||
|
||||
it("does not overwrite existing models directory", async () => {
|
||||
// Pre-create the custom models dir with different content.
|
||||
await fs.mkdir(customModelsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(customModelsDir, "custom-model.bin"), "custom");
|
||||
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
|
||||
// Should still be a real directory, not a symlink.
|
||||
const stat = await fs.lstat(customModelsDir);
|
||||
expect(stat.isSymbolicLink()).toBe(false);
|
||||
expect(stat.isDirectory()).toBe(true);
|
||||
|
||||
// Custom content should be preserved.
|
||||
const content = await fs.readFile(path.join(customModelsDir, "custom-model.bin"), "utf-8");
|
||||
expect(content).toBe("custom");
|
||||
|
||||
await manager!.close();
|
||||
});
|
||||
|
||||
it("skips symlink when no default models exist", async () => {
|
||||
// Remove the default models dir.
|
||||
await fs.rm(defaultModelsDir, { recursive: true, force: true });
|
||||
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||
expect(manager).toBeTruthy();
|
||||
|
||||
// Custom models dir should not exist (no symlink created).
|
||||
await expect(fs.lstat(customModelsDir)).rejects.toThrow();
|
||||
expect(logWarnMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("failed to symlink qmd models directory"),
|
||||
);
|
||||
|
||||
await manager!.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForCondition(check: () => boolean, timeoutMs: number): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user