fix(memory): export archived qmd session transcripts (#57446)

* fix(memory): export archived qmd session transcripts

* test(memory): separate qmd session listing describe
This commit is contained in:
Vincent Koc
2026-03-29 20:50:21 -07:00
committed by GitHub
parent c7106c4285
commit b7d59f7831
3 changed files with 50 additions and 12 deletions

View File

@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
- Agents/MCP: reuse bundled MCP runtimes across turns in the same session, while recreating them when MCP config changes and disposing stale runtimes cleanly on session rollover. (#55090) Thanks @allan0509.
- Memory/QMD: honor `memory.qmd.update.embedInterval` even when regular QMD update cadence is disabled or slower by arming a dedicated embed-cadence maintenance timer, while avoiding redundant timers when regular updates are already frequent enough. (#37326) Thanks @barronlroth.
- Memory/QMD: add `memory.qmd.searchTool` as an exact mcporter tool override, so custom QMD MCP tools such as `hybrid_search` can be used without weakening the validated `searchMode` config surface. (#27801) Thanks @keramblock.
- Memory/QMD: keep reset and deleted session transcripts in QMD session export so daily session resets do not silently drop most historical recall from `memory_search`. (#30220) Thanks @pushkarsingh32.
- Memory/QMD: rebind collections when QMD reports a changed pattern but omits path metadata, so config pattern changes stop being silently ignored on restart. (#49897) Thanks @Madruru.
- Agents/memory flush: keep daily memory flush files append-only during embedded attempts so compaction writes do not overwrite earlier notes. (#53725) Thanks @HPluseven.
- Web UI/markdown: stop bare auto-links from swallowing adjacent CJK text while preserving valid mixed-script path and query characters in rendered links. (#48410) Thanks @jnuyao.

View File

@@ -2,19 +2,55 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { buildSessionEntry } from "./session-files.js";
import { buildSessionEntry, listSessionFilesForAgent } from "./session-files.js";
let tmpDir: string;
let originalStateDir: string | undefined;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "session-entry-test-"));
originalStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = tmpDir;
});
afterEach(async () => {
if (originalStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = originalStateDir;
}
await fs.rm(tmpDir, { recursive: true, force: true });
});
describe("listSessionFilesForAgent", () => {
it("includes reset and deleted transcripts in session file listing", async () => {
const sessionsDir = path.join(tmpDir, "agents", "main", "sessions");
await fs.mkdir(path.join(sessionsDir, "archive"), { recursive: true });
const included = [
"active.jsonl",
"active.jsonl.reset.2026-02-16T22-26-33.000Z",
"active.jsonl.deleted.2026-02-16T22-27-33.000Z",
];
const excluded = ["active.jsonl.bak.2026-02-16T22-28-33.000Z", "sessions.json", "notes.md"];
for (const fileName of [...included, ...excluded]) {
await fs.writeFile(path.join(sessionsDir, fileName), "");
}
await fs.writeFile(
path.join(sessionsDir, "archive", "nested.jsonl.deleted.2026-02-16T22-29-33.000Z"),
"",
);
const files = await listSessionFilesForAgent("main");
expect(files.map((filePath) => path.basename(filePath)).toSorted()).toEqual(
included.toSorted(),
);
});
});
describe("buildSessionEntry", () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "session-entry-test-"));
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
it("returns lineMap tracking original JSONL line numbers", async () => {
// Simulate a real session JSONL file with metadata records interspersed
// Lines 1-3: non-message metadata records

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { isUsageCountedSessionTranscriptFileName } from "../../../../src/config/sessions/artifacts.js";
import { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js";
import { redactSensitiveText } from "../../../../src/logging/redact.js";
import { createSubsystemLogger } from "../../../../src/logging/subsystem.js";
@@ -25,7 +26,7 @@ export async function listSessionFilesForAgent(agentId: string): Promise<string[
return entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((name) => name.endsWith(".jsonl"))
.filter((name) => isUsageCountedSessionTranscriptFileName(name))
.map((name) => path.join(dir, name));
} catch {
return [];