mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:28:27 +00:00
Memory/QMD: skip unchanged session export writes
This commit is contained in:
@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
||||||
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
- Memory/QMD: pass result limits to `search`/`vsearch` commands so QMD can cap results earlier.
|
||||||
- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads.
|
- Memory/QMD: avoid reading full markdown files when a `from/lines` window is requested in QMD reads.
|
||||||
|
- Memory/QMD: skip rewriting unchanged session export markdown files during sync to reduce disk churn.
|
||||||
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
|
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|||||||
@@ -809,6 +809,58 @@ describe("QmdMemoryManager", () => {
|
|||||||
readFileSpy.mockRestore();
|
readFileSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reuses exported session markdown files when inputs are unchanged", async () => {
|
||||||
|
const writeFileSpy = vi.spyOn(fs, "writeFile");
|
||||||
|
const sessionsDir = path.join(stateDir, "agents", agentId, "sessions");
|
||||||
|
await fs.mkdir(sessionsDir, { recursive: true });
|
||||||
|
const sessionFile = path.join(sessionsDir, "session-1.jsonl");
|
||||||
|
await fs.writeFile(
|
||||||
|
sessionFile,
|
||||||
|
'{"type":"message","message":{"role":"user","content":"hello"}}\n',
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
...cfg,
|
||||||
|
memory: {
|
||||||
|
...cfg.memory,
|
||||||
|
qmd: {
|
||||||
|
...cfg.memory.qmd,
|
||||||
|
sessions: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||||
|
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||||
|
expect(manager).toBeTruthy();
|
||||||
|
if (!manager) {
|
||||||
|
throw new Error("manager missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasonCount = writeFileSpy.mock.calls.length;
|
||||||
|
await manager.sync({ reason: "manual" });
|
||||||
|
const firstExportWrites = writeFileSpy.mock.calls.length;
|
||||||
|
expect(firstExportWrites).toBe(reasonCount + 1);
|
||||||
|
|
||||||
|
await manager.sync({ reason: "manual" });
|
||||||
|
expect(writeFileSpy.mock.calls.length).toBe(firstExportWrites);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
await fs.writeFile(
|
||||||
|
sessionFile,
|
||||||
|
'{"type":"message","message":{"role":"user","content":"follow-up update"}}\n',
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
await manager.sync({ reason: "manual" });
|
||||||
|
expect(writeFileSpy.mock.calls.length).toBe(firstExportWrites + 1);
|
||||||
|
|
||||||
|
await manager.close();
|
||||||
|
writeFileSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("throws when sqlite index is busy", async () => {
|
it("throws when sqlite index is busy", async () => {
|
||||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||||
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
string,
|
string,
|
||||||
{ rel: string; abs: string; source: MemorySource }
|
{ rel: string; abs: string; source: MemorySource }
|
||||||
>();
|
>();
|
||||||
|
private readonly exportedSessionState = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
hash: string;
|
||||||
|
mtimeMs: number;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
>();
|
||||||
private readonly maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
private readonly maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
||||||
private readonly sessionExporter: SessionExporterConfig | null;
|
private readonly sessionExporter: SessionExporterConfig | null;
|
||||||
private updateTimer: NodeJS.Timeout | null = null;
|
private updateTimer: NodeJS.Timeout | null = null;
|
||||||
@@ -662,6 +670,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
await fs.mkdir(exportDir, { recursive: true });
|
await fs.mkdir(exportDir, { recursive: true });
|
||||||
const files = await listSessionFilesForAgent(this.agentId);
|
const files = await listSessionFilesForAgent(this.agentId);
|
||||||
const keep = new Set<string>();
|
const keep = new Set<string>();
|
||||||
|
const tracked = new Set<string>();
|
||||||
const cutoff = this.sessionExporter.retentionMs
|
const cutoff = this.sessionExporter.retentionMs
|
||||||
? Date.now() - this.sessionExporter.retentionMs
|
? Date.now() - this.sessionExporter.retentionMs
|
||||||
: null;
|
: null;
|
||||||
@@ -674,7 +683,16 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const target = path.join(exportDir, `${path.basename(sessionFile, ".jsonl")}.md`);
|
const target = path.join(exportDir, `${path.basename(sessionFile, ".jsonl")}.md`);
|
||||||
|
tracked.add(sessionFile);
|
||||||
|
const state = this.exportedSessionState.get(sessionFile);
|
||||||
|
if (!state || state.hash !== entry.hash || state.mtimeMs !== entry.mtimeMs) {
|
||||||
await fs.writeFile(target, this.renderSessionMarkdown(entry), "utf-8");
|
await fs.writeFile(target, this.renderSessionMarkdown(entry), "utf-8");
|
||||||
|
}
|
||||||
|
this.exportedSessionState.set(sessionFile, {
|
||||||
|
hash: entry.hash,
|
||||||
|
mtimeMs: entry.mtimeMs,
|
||||||
|
target,
|
||||||
|
});
|
||||||
keep.add(target);
|
keep.add(target);
|
||||||
}
|
}
|
||||||
const exported = await fs.readdir(exportDir).catch(() => []);
|
const exported = await fs.readdir(exportDir).catch(() => []);
|
||||||
@@ -687,6 +705,11 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
await fs.rm(full, { force: true });
|
await fs.rm(full, { force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const [sessionFile, state] of this.exportedSessionState) {
|
||||||
|
if (!tracked.has(sessionFile) || !state.target.startsWith(exportDir + path.sep)) {
|
||||||
|
this.exportedSessionState.delete(sessionFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSessionMarkdown(entry: SessionFileEntry): string {
|
private renderSessionMarkdown(entry: SessionFileEntry): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user