memory: tighten dreaming bot triage fixes

This commit is contained in:
Gustavo Madeira Santana
2026-04-14 19:30:03 -04:00
parent a3fcb37f50
commit 1b419dc1ad
5 changed files with 77 additions and 9 deletions

View File

@@ -713,8 +713,8 @@ describe("memory-core dreaming phases", () => {
expect(Object.keys(sessionIngestion.files)).toHaveLength(1);
expect(Object.values(sessionIngestion.files)).toEqual([
expect.objectContaining({
lineCount: 2,
lastContentLine: 2,
lineCount: 0,
lastContentLine: 0,
contentHash: expect.any(String),
}),
]);

View File

@@ -230,6 +230,38 @@ describe("short-term promotion", () => {
});
});
it("ignores bullet-prefixed dreaming snippets when recording short-term recalls", async () => {
await withTempWorkspace(async (workspaceDir) => {
await recordShortTermRecalls({
workspaceDir,
query: "action preference",
results: [
{
path: "memory/2026-04-03.md",
source: "memory",
startLine: 1,
endLine: 5,
score: 0.92,
snippet: [
"- Candidate: Default to action.",
" - confidence: 0.76",
" - evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1",
" - recalls: 3",
" - status: staged",
].join("\n"),
},
],
});
expect(
JSON.parse(await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8")),
).toMatchObject({
version: 1,
entries: {},
});
});
});
it("records recalls and ranks candidates with weighted scores", async () => {
await withTempWorkspace(async (workspaceDir) => {
await recordShortTermRecalls({
@@ -1015,6 +1047,14 @@ describe("short-term promotion", () => {
});
});
it("treats diff-prefixed dreaming snippets as contaminated", () => {
expect(
__testing.isContaminatedDreamingSnippet(
"@@ -1,1 - Candidate: Default to action. confidence: 0.76 evidence: memory/.dreams/session-corpus/2026-04-08.txt:1-1 recalls: 3 status: staged",
),
).toBe(true);
});
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
await withTempWorkspace(async (workspaceDir) => {
const applied = await applyShortTermPromotions({

View File

@@ -33,12 +33,16 @@ const SHORT_TERM_LOCK_WAIT_TIMEOUT_MS = 10_000;
const SHORT_TERM_LOCK_STALE_MS = 60_000;
const SHORT_TERM_LOCK_RETRY_DELAY_MS = 40;
const DREAMING_NARRATIVE_PROMPT_PREFIX = "Write a dream diary entry from these memory fragments";
const DREAMING_PROMOTION_META_PREFIX = "openclaw-memory-promotion:";
// Repeated dreaming revisits should be able to clear the default promotion gate
// without requiring separate organic recall traffic for the same snippet.
const PHASE_SIGNAL_LIGHT_BOOST_MAX = 0.06;
const PHASE_SIGNAL_REM_BOOST_MAX = 0.09;
const PHASE_SIGNAL_HALF_LIFE_DAYS = 14;
const DREAMING_NARRATIVE_RUN_PREFIX = "dreaming-narrative-";
const DREAMING_CANDIDATE_LEAD_RE =
/^(?:[-*+>]\s+|@@\s*-\d+(?:,\d+)?\s+[-*+]\s+|[([]\s*)*Candidate:/i;
const DREAMING_REFLECTION_LEAD_RE =
/^(?:[-*+>]\s+|@@\s*-\d+(?:,\d+)?\s+[-*+]\s+|[([]\s*)*Reflections?:/i;
const inProcessShortTermLocks = new Map<string, Promise<void>>();
const ensuredShortTermDirs = new Map<string, Promise<void>>();
@@ -244,15 +248,14 @@ function isContaminatedDreamingSnippet(raw: string): boolean {
}
if (
snippet.includes(DREAMING_NARRATIVE_PROMPT_PREFIX) ||
snippet.includes(DREAMING_PROMOTION_META_PREFIX) ||
snippet.includes("dreaming-narrative-")
snippet.includes(PROMOTION_MARKER_PREFIX) ||
snippet.includes(DREAMING_NARRATIVE_RUN_PREFIX)
) {
return true;
}
const hasCandidateLead = /^Candidate:/i.test(snippet) || /[([]\s*Candidate:/i.test(snippet);
const hasReflectionLead =
/^Reflections?:/i.test(snippet) || /[([]\s*Reflections?:/i.test(snippet);
const hasCandidateLead = DREAMING_CANDIDATE_LEAD_RE.test(snippet);
const hasReflectionLead = DREAMING_REFLECTION_LEAD_RE.test(snippet);
const hasConfidence = /\bconfidence:\s*\d/i.test(snippet);
const hasEvidence = /\bevidence:\s*(?:memory\/\.dreams\/session-corpus\/|memory\/)/i.test(
snippet,

View File

@@ -203,4 +203,29 @@ describe("buildSessionEntry", () => {
expect(entry?.content).toContain("Assistant: A drifting archive breathed in moonlight.");
expect(entry?.lineMap).toEqual([1, 2]);
});
it("does not flag transcripts when dreaming markers only appear mid-string", async () => {
const jsonlLines = [
JSON.stringify({
type: "custom",
customType: "note",
data: {
runId: "user-context-dreaming-narrative-light-1775894400455",
},
}),
JSON.stringify({
type: "message",
message: { role: "user", content: "Keep the archive index updated." },
}),
];
const filePath = path.join(tmpDir, "substring-marker-session.jsonl");
await fs.writeFile(filePath, jsonlLines.join("\n"));
const entry = await buildSessionEntry(filePath);
expect(entry).not.toBeNull();
expect(entry?.generatedByDreamingNarrative).toBeUndefined();
expect(entry?.content).toContain("User: Keep the archive index updated.");
expect(entry?.lineMap).toEqual([2]);
});
});

View File

@@ -47,7 +47,7 @@ function isDreamingNarrativeBootstrapRecord(record: unknown): boolean {
}
function hasDreamingNarrativeRunId(value: unknown): boolean {
return typeof value === "string" && value.includes(DREAMING_NARRATIVE_RUN_PREFIX);
return typeof value === "string" && value.startsWith(DREAMING_NARRATIVE_RUN_PREFIX);
}
function isDreamingNarrativeGeneratedRecord(record: unknown): boolean {