Skip to content

Commit 144693f

Browse files
committed
fix(memory): skip placeholder short-term promotions
1 parent 43901be commit 144693f

4 files changed

Lines changed: 670 additions & 63 deletions

File tree

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

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ vi.mock("openclaw/plugin-sdk/memory-host-events", () => ({
1111
appendMemoryHostEvent: vi.fn(async () => {}),
1212
}));
1313

14+
import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events";
1415
import {
1516
configureMemoryCoreDreamingState,
1617
configureMemoryCoreDreamingStateForTests,
@@ -1620,6 +1621,273 @@ describe("short-term promotion", () => {
16201621
});
16211622
});
16221623

1624+
it("classifies markdown placeholders without dropping meaningful snippets", () => {
1625+
for (const raw of [
1626+
"-",
1627+
"* ",
1628+
"- -",
1629+
">",
1630+
"```",
1631+
"```ts",
1632+
"``` ts",
1633+
"~~~ qmd",
1634+
"| --- |",
1635+
"- [ ]",
1636+
"- [x]",
1637+
"@@ -1,2\n## Tagesnotizen\n-",
1638+
"@@ -1,2 ## Tagesnotizen -",
1639+
"## Tagesnotizen\n-\n\n## Entscheidungen:\n-",
1640+
"## Tagesnotizen ## Entscheidungen",
1641+
"## Morning ## Light Sleep",
1642+
"## Tagesnotizen ## Morning",
1643+
"## 2026-05-28 ## Morning",
1644+
"# 2026-05-28",
1645+
"@@ -1,2 # 2026-05-28 -",
1646+
"## Morning",
1647+
"## Morning:",
1648+
"@@ -1,2 ## Morning -",
1649+
"@@ -1,2 ## Morning: -",
1650+
"## Light Sleep",
1651+
"## Light Sleep:",
1652+
"Morning:",
1653+
"Light Sleep:",
1654+
]) {
1655+
expect(testing.resolvePromotableShortTermSnippet(raw)).toMatchObject({
1656+
promotable: false,
1657+
reason: "markdown-placeholder",
1658+
});
1659+
}
1660+
1661+
for (const raw of [
1662+
"- Keep gateway restarts supervised.",
1663+
"- [ ] rotate API keys",
1664+
"- [x] backups verified",
1665+
"## API keys rotated",
1666+
"## Decision: Move backups to S3",
1667+
"## Morning backup verification",
1668+
"## Morning - backup verification",
1669+
"@@ -7,1\nrouter glacier backup",
1670+
"@@ -1,1\n- Keep gateway restarts supervised.",
1671+
"- ## 修复备份轮换",
1672+
]) {
1673+
expect(testing.resolvePromotableShortTermSnippet(raw)).toMatchObject({
1674+
promotable: true,
1675+
});
1676+
}
1677+
1678+
expect(
1679+
testing.resolvePromotableShortTermSnippet(
1680+
"@@ -6,2\n## Morning\n- Reviewed travel timing before the workshop.",
1681+
),
1682+
).toMatchObject({
1683+
promotable: true,
1684+
snippet: "- Reviewed travel timing before the workshop.",
1685+
});
1686+
expect(
1687+
testing.resolvePromotableShortTermSnippet(
1688+
"@@ -6,2 ## Morning - Reviewed travel timing before the workshop.",
1689+
),
1690+
).toMatchObject({
1691+
promotable: true,
1692+
snippet: "Reviewed travel timing before the workshop.",
1693+
});
1694+
expect(
1695+
testing.resolvePromotableShortTermSnippet(
1696+
"@@ -6,2 ## Tagesnotizen - Rotate backups before deploy.",
1697+
),
1698+
).toMatchObject({
1699+
promotable: true,
1700+
snippet: "Rotate backups before deploy.",
1701+
});
1702+
expect(
1703+
testing.resolvePromotableShortTermSnippet("@@ -7,1 router glacier backup"),
1704+
).toMatchObject({
1705+
promotable: true,
1706+
snippet: "router glacier backup",
1707+
});
1708+
expect(
1709+
testing.resolvePromotableShortTermSnippet("## Morning - backup verification"),
1710+
).toMatchObject({
1711+
promotable: true,
1712+
snippet: "backup verification",
1713+
});
1714+
expect(
1715+
testing.resolvePromotableShortTermSnippet("## Morning: - backup verification"),
1716+
).toMatchObject({
1717+
promotable: true,
1718+
snippet: "backup verification",
1719+
});
1720+
expect(
1721+
testing.resolvePromotableShortTermSnippet("Morning:: Reviewed travel timing."),
1722+
).toMatchObject({
1723+
promotable: true,
1724+
snippet: "Reviewed travel timing.",
1725+
});
1726+
expect(
1727+
testing.resolvePromotableShortTermSnippet("Light Sleep: useful dream note"),
1728+
).toMatchObject({
1729+
promotable: true,
1730+
snippet: "useful dream note",
1731+
});
1732+
expect(
1733+
testing.resolvePromotableShortTermSnippet("- Morning: Reviewed travel timing."),
1734+
).toMatchObject({
1735+
promotable: true,
1736+
snippet: "Reviewed travel timing.",
1737+
});
1738+
expect(
1739+
testing.resolvePromotableShortTermSnippet("@@ -6,1 - Light Sleep: useful note"),
1740+
).toMatchObject({
1741+
promotable: true,
1742+
snippet: "useful note",
1743+
});
1744+
expect(testing.resolvePromotableShortTermSnippet("Decision: Move backups to S3")).toMatchObject(
1745+
{
1746+
promotable: true,
1747+
snippet: "Decision: Move backups to S3",
1748+
},
1749+
);
1750+
expect(
1751+
testing.resolvePromotableShortTermSnippet("- Decision: Move backups to S3"),
1752+
).toMatchObject({
1753+
promotable: true,
1754+
snippet: "- Decision: Move backups to S3",
1755+
});
1756+
expect(
1757+
testing.resolvePromotableShortTermSnippet(
1758+
"~~~ qmd\n- Keep gateway restarts supervised.\n~~~",
1759+
),
1760+
).toMatchObject({
1761+
promotable: true,
1762+
snippet: "- Keep gateway restarts supervised.",
1763+
});
1764+
});
1765+
1766+
it("skips placeholder short-term recalls before recording and reports them separately", async () => {
1767+
await withTempWorkspace(async (workspaceDir) => {
1768+
const notePath = await writeDailyMemoryNote(workspaceDir, "2026-04-19", [
1769+
"## Tagesnotizen",
1770+
"-",
1771+
"Rotate backups before deploy.",
1772+
]);
1773+
const relativePath = path.relative(workspaceDir, notePath).replaceAll("\\", "/");
1774+
const appendEventMock = vi.mocked(appendMemoryHostEvent);
1775+
appendEventMock.mockClear();
1776+
1777+
await recordShortTermRecalls({
1778+
workspaceDir,
1779+
query: "backup recall",
1780+
results: [
1781+
{
1782+
path: relativePath,
1783+
source: "memory",
1784+
startLine: 1,
1785+
endLine: 2,
1786+
score: 0.8,
1787+
snippet: "@@ -1,2\n## Tagesnotizen\n-",
1788+
},
1789+
{
1790+
path: relativePath,
1791+
source: "memory",
1792+
startLine: 3,
1793+
endLine: 3,
1794+
score: 0.9,
1795+
snippet: "Rotate backups before deploy.",
1796+
},
1797+
],
1798+
});
1799+
1800+
const entries = Object.values(await readRecallStoreEntries(workspaceDir));
1801+
expect(entries).toHaveLength(1);
1802+
expect(readEntrySnippet(entries[0])).toBe("Rotate backups before deploy.");
1803+
1804+
const events = appendEventMock.mock.calls.map(
1805+
([, event]) =>
1806+
event as {
1807+
type?: string;
1808+
reason?: string;
1809+
resultCount?: number;
1810+
skippedResultCount?: number;
1811+
results?: unknown[];
1812+
},
1813+
);
1814+
expect(events.find((event) => event.type === "memory.recall.recorded")).toMatchObject({
1815+
resultCount: 1,
1816+
results: [expect.objectContaining({ startLine: 3, endLine: 3 })],
1817+
});
1818+
expect(
1819+
events.find(
1820+
(event) =>
1821+
event.type === "memory.recall.skipped" &&
1822+
event.reason === "unpromotable-short-term-snippet",
1823+
),
1824+
).toMatchObject({
1825+
skippedResultCount: 1,
1826+
results: [expect.objectContaining({ startLine: 1, endLine: 2 })],
1827+
});
1828+
});
1829+
});
1830+
1831+
it("drops placeholder snippets from existing and grounded short-term candidates", async () => {
1832+
await withTempWorkspace(async (workspaceDir) => {
1833+
await testing.writeRawRecallStore(workspaceDir, {
1834+
version: 1,
1835+
updatedAt: "2026-04-04T00:00:00.000Z",
1836+
entries: {
1837+
placeholder: {
1838+
key: "placeholder",
1839+
path: "memory/2026-04-03.md",
1840+
startLine: 1,
1841+
endLine: 2,
1842+
source: "memory",
1843+
snippet: "## Tagesnotizen\n-",
1844+
recallCount: 4,
1845+
dailyCount: 0,
1846+
groundedCount: 0,
1847+
totalScore: 3.6,
1848+
maxScore: 0.95,
1849+
firstRecalledAt: "2026-04-03T00:00:00.000Z",
1850+
lastRecalledAt: "2026-04-04T00:00:00.000Z",
1851+
queryHashes: ["a", "b"],
1852+
recallDays: ["2026-04-03", "2026-04-04"],
1853+
conceptTags: ["notes"],
1854+
},
1855+
},
1856+
});
1857+
await recordGroundedShortTermCandidates({
1858+
workspaceDir,
1859+
query: "backup recall",
1860+
items: [
1861+
{
1862+
path: "memory/2026-04-04.md",
1863+
startLine: 1,
1864+
endLine: 2,
1865+
score: 0.95,
1866+
snippet: "## Entscheidungen:\n-",
1867+
},
1868+
{
1869+
path: "memory/2026-04-04.md",
1870+
startLine: 3,
1871+
endLine: 3,
1872+
score: 0.95,
1873+
snippet: "- Keep gateway restarts supervised.",
1874+
},
1875+
],
1876+
});
1877+
1878+
const ranked = await rankShortTermPromotionCandidates({
1879+
workspaceDir,
1880+
minScore: 0,
1881+
minRecallCount: 0,
1882+
minUniqueQueries: 0,
1883+
});
1884+
1885+
expect(ranked.map((candidate) => candidate.snippet)).toStrictEqual([
1886+
"- Keep gateway restarts supervised.",
1887+
]);
1888+
});
1889+
});
1890+
16231891
it("treats diff-prefixed dreaming snippets as contaminated", () => {
16241892
expect(
16251893
testing.isContaminatedDreamingSnippet(
@@ -1957,6 +2225,48 @@ describe("short-term promotion", () => {
19572225
});
19582226
});
19592227

2228+
it("refuses placeholder-only direct promotion candidates", async () => {
2229+
await withTempWorkspace(async (workspaceDir) => {
2230+
const applied = await applyShortTermPromotions({
2231+
workspaceDir,
2232+
minScore: 0,
2233+
minRecallCount: 0,
2234+
minUniqueQueries: 0,
2235+
candidates: [
2236+
{
2237+
key: "memory:memory/2026-04-19.md:2:2",
2238+
path: "memory/2026-04-19.md",
2239+
startLine: 2,
2240+
endLine: 2,
2241+
source: "memory",
2242+
snippet: "-",
2243+
recallCount: 3,
2244+
avgScore: 0.9,
2245+
maxScore: 0.9,
2246+
uniqueQueries: 2,
2247+
firstRecalledAt: "2026-04-18T00:00:00.000Z",
2248+
lastRecalledAt: "2026-04-19T00:00:00.000Z",
2249+
ageDays: 1,
2250+
score: 0.9,
2251+
recallDays: ["2026-04-18", "2026-04-19"],
2252+
conceptTags: ["notes"],
2253+
components: {
2254+
frequency: 1,
2255+
relevance: 0.9,
2256+
diversity: 1,
2257+
recency: 1,
2258+
consolidation: 0.5,
2259+
conceptual: 0.2,
2260+
},
2261+
},
2262+
],
2263+
});
2264+
2265+
expect(applied.applied).toBe(0);
2266+
await expectEnoent(fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"));
2267+
});
2268+
});
2269+
19602270
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
19612271
await withTempWorkspace(async (workspaceDir) => {
19622272
const applied = await applyShortTermPromotions({

0 commit comments

Comments
 (0)