mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 04:37:40 +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:
@@ -144,6 +144,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
||||
await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
|
||||
|
||||
// QMD stores its ML models under $XDG_CACHE_HOME/qmd/models/. Because we
|
||||
// override XDG_CACHE_HOME to isolate the index per-agent, qmd would not
|
||||
// find models installed at the default location (~/.cache/qmd/models/) and
|
||||
// would attempt to re-download them on every invocation. Symlink the
|
||||
// default models directory into our custom cache so the index stays
|
||||
// isolated while models are shared.
|
||||
await this.symlinkSharedModels();
|
||||
|
||||
this.bootstrapCollections();
|
||||
await this.ensureCollections();
|
||||
|
||||
@@ -465,6 +473,68 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Symlink the default QMD models directory into our custom XDG_CACHE_HOME so
|
||||
* that the pre-installed ML models (~/.cache/qmd/models/) are reused rather
|
||||
* than re-downloaded for every agent. If the default models directory does
|
||||
* not exist, or a models directory/symlink already exists in the target, this
|
||||
* is a no-op.
|
||||
*/
|
||||
private async symlinkSharedModels(): Promise<void> {
|
||||
// process.env is never modified — only this.env (passed to child_process
|
||||
// spawn) overrides XDG_CACHE_HOME. So reading it here gives us the
|
||||
// user's original value, which is where `qmd` downloaded its models.
|
||||
//
|
||||
// On Windows, well-behaved apps (including Rust `dirs` / Go os.UserCacheDir)
|
||||
// store caches under %LOCALAPPDATA% rather than ~/.cache. Fall back to
|
||||
// LOCALAPPDATA when XDG_CACHE_HOME is not set on Windows.
|
||||
const defaultCacheHome =
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
(process.platform === "win32" ? process.env.LOCALAPPDATA : undefined) ||
|
||||
path.join(os.homedir(), ".cache");
|
||||
const defaultModelsDir = path.join(defaultCacheHome, "qmd", "models");
|
||||
const targetModelsDir = path.join(this.xdgCacheHome, "qmd", "models");
|
||||
try {
|
||||
// Check if the default models directory exists.
|
||||
// Missing path is normal on first run and should be silent.
|
||||
const stat = await fs.stat(defaultModelsDir).catch((err: unknown) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!stat?.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
// Check if something already exists at the target path
|
||||
try {
|
||||
await fs.lstat(targetModelsDir);
|
||||
// Already exists (directory, symlink, or file) – leave it alone
|
||||
return;
|
||||
} catch {
|
||||
// Does not exist – proceed to create symlink
|
||||
}
|
||||
// On Windows, creating directory symlinks requires either Administrator
|
||||
// privileges or Developer Mode. Fall back to a directory junction which
|
||||
// works without elevated privileges (junctions are always absolute-path,
|
||||
// which is fine here since both paths are already absolute).
|
||||
try {
|
||||
await fs.symlink(defaultModelsDir, targetModelsDir, "dir");
|
||||
} catch (symlinkErr: unknown) {
|
||||
const code = (symlinkErr as NodeJS.ErrnoException).code;
|
||||
if (process.platform === "win32" && (code === "EPERM" || code === "ENOTSUP")) {
|
||||
await fs.symlink(defaultModelsDir, targetModelsDir, "junction");
|
||||
} else {
|
||||
throw symlinkErr;
|
||||
}
|
||||
}
|
||||
log.debug(`symlinked qmd models: ${defaultModelsDir} → ${targetModelsDir}`);
|
||||
} catch (err) {
|
||||
// Non-fatal: if we can't symlink, qmd will fall back to downloading
|
||||
log.warn(`failed to symlink qmd models directory: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async runQmd(
|
||||
args: string[],
|
||||
opts?: { timeoutMs?: number },
|
||||
|
||||
Reference in New Issue
Block a user