Skip to content

Commit 211d0d1

Browse files
committed
fix(memory): skip empty dreaming placeholders
1 parent 4336340 commit 211d0d1

4 files changed

Lines changed: 90 additions & 8 deletions

File tree

extensions/memory-core/src/dreaming-phases.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,19 @@ describe("memory-core dreaming phases", () => {
14871487
expect(corpus).not.toContain("Run the qmd sync");
14881488
});
14891489

1490+
it("omits possible lasting truths section when rem finds no candidate truths", () => {
1491+
const preview = __testing.previewRemDreaming({
1492+
entries: [],
1493+
limit: 5,
1494+
minPatternStrength: 0,
1495+
});
1496+
1497+
expect(preview.candidateTruths).toStrictEqual([]);
1498+
expect(preview.bodyLines.join("\n")).toContain("### Reflections");
1499+
expect(preview.bodyLines.join("\n")).not.toContain("### Possible Lasting Truths");
1500+
expect(preview.bodyLines.join("\n")).not.toContain("No strong candidate truths surfaced");
1501+
});
1502+
14901503
it("ignores chat scaffolding tags when building rem reflections", () => {
14911504
const preview = __testing.previewRemDreaming({
14921505
entries: [

extensions/memory-core/src/dreaming-phases.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,14 +1532,16 @@ export function previewRemDreaming(params: {
15321532
const bodyLines = [
15331533
"### Reflections",
15341534
...reflections,
1535-
"",
1536-
"### Possible Lasting Truths",
15371535
...(candidateTruths.length > 0
1538-
? candidateTruths.map(
1539-
(entry) =>
1540-
`- ${entry.snippet} [confidence=${entry.confidence.toFixed(2)} evidence=${entry.evidence}]`,
1541-
)
1542-
: ["- No strong candidate truths surfaced."]),
1536+
? [
1537+
"",
1538+
"### Possible Lasting Truths",
1539+
...candidateTruths.map(
1540+
(entry) =>
1541+
`- ${entry.snippet} [confidence=${entry.confidence.toFixed(2)} evidence=${entry.evidence}]`,
1542+
),
1543+
]
1544+
: []),
15431545
];
15441546
return {
15451547
sourceEntryCount: params.entries.length,

extensions/memory-core/src/short-term-promotion.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,17 @@ describe("short-term promotion", () => {
11101110
).toBe(false);
11111111
});
11121112

1113+
it("treats empty rem candidate placeholders as contaminated", () => {
1114+
expect(
1115+
__testing.isEmptyDreamingCandidatePlaceholder("- No strong candidate truths surfaced."),
1116+
).toBe(true);
1117+
expect(
1118+
__testing.isContaminatedDreamingSnippet(
1119+
"- Candidate: Possible Lasting Truths: No strong candidate truths surfaced.",
1120+
),
1121+
).toBe(true);
1122+
});
1123+
11131124
it("treats transcript-style dreaming prompt echoes as contaminated", () => {
11141125
expect(
11151126
__testing.isContaminatedDreamingSnippet(
@@ -1118,6 +1129,48 @@ describe("short-term promotion", () => {
11181129
).toBe(true);
11191130
});
11201131

1132+
it("skips empty rem candidate placeholders during apply", async () => {
1133+
await withTempWorkspace(async (workspaceDir) => {
1134+
const applied = await applyShortTermPromotions({
1135+
workspaceDir,
1136+
minScore: 0,
1137+
minRecallCount: 0,
1138+
minUniqueQueries: 0,
1139+
candidates: [
1140+
{
1141+
key: "memory:memory/2026-04-08.md:13:13",
1142+
path: "memory/2026-04-08.md",
1143+
startLine: 13,
1144+
endLine: 13,
1145+
source: "memory",
1146+
snippet: "- Candidate: Possible Lasting Truths: No strong candidate truths surfaced.",
1147+
recallCount: 3,
1148+
avgScore: 0.95,
1149+
maxScore: 0.95,
1150+
uniqueQueries: 2,
1151+
firstRecalledAt: "2026-04-08T00:00:00.000Z",
1152+
lastRecalledAt: "2026-04-09T00:00:00.000Z",
1153+
ageDays: 0,
1154+
score: 0.95,
1155+
recallDays: ["2026-04-08", "2026-04-09"],
1156+
conceptTags: ["candidate", "truths"],
1157+
components: {
1158+
frequency: 1,
1159+
relevance: 1,
1160+
diversity: 1,
1161+
recency: 1,
1162+
consolidation: 1,
1163+
conceptual: 1,
1164+
},
1165+
},
1166+
],
1167+
});
1168+
1169+
expect(applied.applied).toBe(0);
1170+
await expectEnoent(fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"));
1171+
});
1172+
});
1173+
11211174
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
11221175
await withTempWorkspace(async (workspaceDir) => {
11231176
const applied = await applyShortTermPromotions({

extensions/memory-core/src/short-term-promotion.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,27 @@ function hasDreamingNarrativeLead(snippet: string): boolean {
287287
return /^Candidate:/i.test(withoutPrefix) || /^Reflections?:/i.test(withoutPrefix);
288288
}
289289

290+
function isEmptyDreamingCandidatePlaceholder(raw: string): boolean {
291+
const snippet = normalizeSnippet(raw);
292+
if (!snippet) {
293+
return false;
294+
}
295+
const withoutPrefix = consumeDreamingLeadPrefix(snippet)
296+
.replace(/^Candidate:\s*/i, "")
297+
.replace(/^Possible Lasting Truths:\s*/i, "")
298+
.trim();
299+
return /^No strong candidate truths surfaced\.?$/i.test(withoutPrefix);
300+
}
301+
290302
function isContaminatedDreamingSnippet(raw: string): boolean {
291303
const snippet = normalizeSnippet(raw);
292304
if (!snippet) {
293305
return false;
294306
}
295307
if (
296308
/<!--\s*openclaw-memory-promotion:/i.test(snippet) ||
297-
DREAMING_TRANSCRIPT_PROMPT_LINE_RE.test(snippet)
309+
DREAMING_TRANSCRIPT_PROMPT_LINE_RE.test(snippet) ||
310+
isEmptyDreamingCandidatePlaceholder(snippet)
298311
) {
299312
return true;
300313
}
@@ -2018,4 +2031,5 @@ export const __testing = {
20182031
buildClaimHash,
20192032
totalSignalCountForEntry,
20202033
isContaminatedDreamingSnippet,
2034+
isEmptyDreamingCandidatePlaceholder,
20212035
};

0 commit comments

Comments
 (0)