openclaw-8f8: skip dreaming transcript ingestion via session store

This commit is contained in:
Josh Lehman
2026-04-15 10:22:18 -07:00
parent 5dcf526a43
commit b99fd53711
3 changed files with 212 additions and 1 deletions

View File

@@ -720,6 +720,119 @@ describe("memory-core dreaming phases", () => {
]);
});
it("skips dreaming transcripts when the session store identifies them before bootstrap lands", async () => {
const workspaceDir = await createDreamingWorkspace();
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state"));
const sessionsDir = resolveSessionTranscriptsDirForAgent("main");
await fs.mkdir(sessionsDir, { recursive: true });
const transcriptPath = path.join(sessionsDir, "dreaming-narrative.jsonl");
await fs.writeFile(
transcriptPath,
[
JSON.stringify({
type: "message",
message: {
role: "user",
timestamp: "2026-04-05T18:01:00.000Z",
content: [
{ type: "text", text: "Write a dream diary entry from these memory fragments." },
],
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
timestamp: "2026-04-05T18:02:00.000Z",
content: [{ type: "text", text: "I drift through the same archive again." }],
},
}),
].join("\n") + "\n",
"utf-8",
);
await fs.writeFile(
path.join(sessionsDir, "sessions.json"),
JSON.stringify({
"agent:main:dreaming-narrative-light-1775894400455": {
sessionId: "dreaming-narrative",
sessionFile: transcriptPath,
updatedAt: Date.parse("2026-04-05T18:05:00.000Z"),
},
}),
"utf-8",
);
const mtime = new Date("2026-04-05T18:05:00.000Z");
await fs.utimes(transcriptPath, mtime, mtime);
const { beforeAgentReply } = createHarness(
{
agents: {
defaults: {
workspace: workspaceDir,
},
list: [{ id: "main", workspace: workspaceDir }],
},
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
enabled: true,
phases: {
light: {
enabled: true,
limit: 20,
lookbackDays: 7,
},
},
},
},
},
},
},
},
workspaceDir,
);
try {
await beforeAgentReply(
{ cleanedBody: "__openclaw_memory_core_light_sleep__" },
{ trigger: "heartbeat", workspaceDir },
);
} finally {
vi.unstubAllEnvs();
}
await expect(
fs.access(path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt")),
).rejects.toMatchObject({ code: "ENOENT" });
const sessionIngestion = JSON.parse(
await fs.readFile(
path.join(workspaceDir, "memory", ".dreams", "session-ingestion.json"),
"utf-8",
),
) as {
files: Record<
string,
{
lineCount: number;
lastContentLine: number;
contentHash: string;
}
>;
};
expect(Object.keys(sessionIngestion.files)).toHaveLength(1);
expect(Object.values(sessionIngestion.files)).toEqual([
expect.objectContaining({
lineCount: 0,
lastContentLine: 0,
contentHash: expect.any(String),
}),
]);
});
it("does not reread unchanged dreaming-generated transcripts after checkpointing skip state", async () => {
const workspaceDir = await createDreamingWorkspace();
vi.stubEnv("OPENCLAW_TEST_FAST", "1");

View File

@@ -175,6 +175,50 @@ describe("buildSessionEntry", () => {
expect(entry?.generatedByDreamingNarrative).toBe(true);
});
it("flags dreaming narrative transcripts from the sibling session store before bootstrap lands", async () => {
const sessionsDir = path.join(tmpDir, "agents", "main", "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
const filePath = path.join(sessionsDir, "dreaming-session.jsonl");
await fs.writeFile(
filePath,
[
JSON.stringify({
type: "message",
message: {
role: "user",
content:
"Write a dream diary entry from these memory fragments:\n- Candidate: durable note",
},
}),
JSON.stringify({
type: "message",
message: {
role: "assistant",
content: "A drifting archive breathed in moonlight.",
},
}),
].join("\n"),
);
await fs.writeFile(
path.join(sessionsDir, "sessions.json"),
JSON.stringify({
"agent:main:dreaming-narrative-light-1775894400455": {
sessionId: "dreaming-session",
sessionFile: filePath,
updatedAt: Date.now(),
},
}),
"utf-8",
);
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.generatedByDreamingNarrative).toBe(true);
expect(entry?.content).toBe("");
expect(entry?.lineMap).toEqual([]);
});
it("does not flag ordinary transcripts that quote the dream-diary prompt", async () => {
const jsonlLines = [
JSON.stringify({

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { isUsageCountedSessionTranscriptFileName } from "../../config/sessions/artifacts.js";
import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js";
import { loadSessionStore } from "../../config/sessions/store-load.js";
import { redactSensitiveText } from "../../logging/redact.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { hashText } from "./internal.js";
@@ -78,6 +79,59 @@ function isDreamingNarrativeGeneratedRecord(record: unknown): boolean {
return hasDreamingNarrativeRunId(nested.runId) || hasDreamingNarrativeRunId(nested.sessionKey);
}
function isDreamingNarrativeSessionStoreKey(sessionKey: string): boolean {
const trimmed = sessionKey.trim();
if (!trimmed) {
return false;
}
const firstSeparator = trimmed.indexOf(":");
if (firstSeparator < 0) {
return trimmed.startsWith(DREAMING_NARRATIVE_RUN_PREFIX);
}
const secondSeparator = trimmed.indexOf(":", firstSeparator + 1);
const sessionSegment = secondSeparator < 0 ? trimmed : trimmed.slice(secondSeparator + 1);
return sessionSegment.startsWith(DREAMING_NARRATIVE_RUN_PREFIX);
}
function normalizeComparablePath(pathname: string): string {
const resolved = path.resolve(pathname);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
function resolveSessionStoreTranscriptPath(
sessionsDir: string,
entry: { sessionFile?: unknown; sessionId?: unknown } | undefined,
): string | null {
if (typeof entry?.sessionFile === "string" && entry.sessionFile.trim().length > 0) {
const sessionFile = entry.sessionFile.trim();
const resolved = path.isAbsolute(sessionFile)
? sessionFile
: path.resolve(sessionsDir, sessionFile);
return normalizeComparablePath(resolved);
}
if (typeof entry?.sessionId === "string" && entry.sessionId.trim().length > 0) {
return normalizeComparablePath(path.join(sessionsDir, `${entry.sessionId.trim()}.jsonl`));
}
return null;
}
function isDreamingNarrativeTranscriptFromSessionStore(absPath: string): boolean {
const sessionsDir = path.dirname(absPath);
const storePath = path.join(sessionsDir, "sessions.json");
const normalizedAbsPath = normalizeComparablePath(absPath);
const store = loadSessionStore(storePath);
for (const [sessionKey, entry] of Object.entries(store)) {
if (!isDreamingNarrativeSessionStoreKey(sessionKey)) {
continue;
}
const transcriptPath = resolveSessionStoreTranscriptPath(sessionsDir, entry);
if (transcriptPath === normalizedAbsPath) {
return true;
}
}
return false;
}
export async function listSessionFilesForAgent(agentId: string): Promise<string[]> {
const dir = resolveSessionTranscriptsDirForAgent(agentId);
try {
@@ -161,7 +215,7 @@ export async function buildSessionEntry(absPath: string): Promise<SessionFileEnt
const collected: string[] = [];
const lineMap: number[] = [];
const messageTimestampsMs: number[] = [];
let generatedByDreamingNarrative = false;
let generatedByDreamingNarrative = isDreamingNarrativeTranscriptFromSessionStore(absPath);
for (let jsonlIdx = 0; jsonlIdx < lines.length; jsonlIdx++) {
const line = lines[jsonlIdx];
if (!line.trim()) {