feat(FN-3197): refresh qmd after agent dream writes and normalize agent-mem
This merge normalizes agent-memory paths in the core memory backend and ensures the qmd gets refreshed after agent dream writes, fixing a bug where stale paths could persist after memory updates. The engine's agent-tools module was updated to integrate with this fix, and a changeset was included for Fusion-Task-Id: FN-3197
This commit is contained in:
@@ -34,6 +34,7 @@ type ExecFileAsync = (
|
||||
) => Promise<{ stdout: string; stderr: string }>;
|
||||
|
||||
const qmdRefreshState = new Map<string, { lastStartedAt: number; inFlight?: Promise<void> }>();
|
||||
const qmdAgentRefreshState = new Map<string, { lastStartedAt: number; inFlight?: Promise<void> }>();
|
||||
let qmdInstallPromise: Promise<boolean> | null = null;
|
||||
|
||||
export function shouldSkipBackgroundQmdRefresh(): boolean {
|
||||
@@ -494,6 +495,13 @@ export function buildQmdRefreshCommands(rootDir: string): string[][] {
|
||||
];
|
||||
}
|
||||
|
||||
export function qmdAgentMemoryCollectionName(rootDir: string, agentId: string): string {
|
||||
const absoluteRoot = resolve(rootDir);
|
||||
const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
||||
const hash = createHash("sha1").update(`${absoluteRoot}:${agentId}`).digest("hex").slice(0, 12);
|
||||
return `fusion-agent-memory-${safeAgentId.toLowerCase()}-${hash}`;
|
||||
}
|
||||
|
||||
export function dailyMemoryPath(rootDir: string, date = new Date()): string {
|
||||
return join(memoryWorkspacePath(rootDir), `${date.toISOString().slice(0, 10)}.md`);
|
||||
}
|
||||
@@ -1100,6 +1108,70 @@ export function scheduleQmdProjectMemoryRefresh(rootDir: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshQmdAgentMemoryIndex(
|
||||
rootDir: string,
|
||||
agentId: string,
|
||||
options?: { force?: boolean; execFileAsync?: ExecFileAsync },
|
||||
): Promise<void> {
|
||||
const key = `${resolve(rootDir)}:${agentId}`;
|
||||
const now = Date.now();
|
||||
const current = qmdAgentRefreshState.get(key);
|
||||
|
||||
if (!options?.force) {
|
||||
if (current?.inFlight) {
|
||||
return current.inFlight;
|
||||
}
|
||||
if (current && now - current.lastStartedAt < QMD_REFRESH_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
const execFileAsync = options?.execFileAsync ?? await getDefaultExecFileAsync();
|
||||
const { agentMemoryWorkspacePath } = await import("./memory-dreams.js");
|
||||
const workspacePath = agentMemoryWorkspacePath(rootDir, agentId);
|
||||
await mkdir(workspacePath, { recursive: true });
|
||||
|
||||
try {
|
||||
await execFileAsync("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
|
||||
cwd: rootDir,
|
||||
timeout: 4000,
|
||||
maxBuffer: 512 * 1024,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stderr = typeof err === "object" && err && "stderr" in err ? String((err as { stderr?: unknown }).stderr ?? "") : "";
|
||||
if (!/already exists|exists/i.test(`${message}\n${stderr}`)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await execFileAsync("qmd", ["update"], { cwd: rootDir, timeout: 30_000, maxBuffer: 1024 * 1024 });
|
||||
await execFileAsync("qmd", ["embed"], { cwd: rootDir, timeout: 120_000, maxBuffer: 1024 * 1024 });
|
||||
})();
|
||||
|
||||
qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
const latest = qmdAgentRefreshState.get(key);
|
||||
if (latest?.inFlight === promise) {
|
||||
qmdAgentRefreshState.set(key, { lastStartedAt: latest.lastStartedAt });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scheduleQmdAgentMemoryRefresh(rootDir: string, agentId: string): void {
|
||||
if (shouldSkipBackgroundQmdRefresh()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshQmdAgentMemoryIndex(rootDir, agentId).catch(() => {
|
||||
// qmd is optional. Search falls back to local file scanning when refresh fails.
|
||||
});
|
||||
}
|
||||
|
||||
export async function isQmdAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const execFileAsync = await getDefaultExecFileAsync();
|
||||
|
||||
Reference in New Issue
Block a user