@@ -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" ;
1415import {
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