@@ -1756,6 +1756,124 @@ describe("short-term promotion", () => {
17561756 expect ( testing . lineRangeOverlapsDreamingFence ( lines , 8 , 8 ) ) . toBe ( false ) ;
17571757 expect ( testing . lineRangeOverlapsDreamingFence ( lines , 6 , 6 ) ) . toBe ( true ) ;
17581758 } ) ;
1759+
1760+ // Marker lines themselves carry managed-block content. A relocated range
1761+ // that includes a `<!-- openclaw:dreaming:*:start/end -->` marker would
1762+ // build its snippet from raw lines that contain that marker text, leaking
1763+ // it into MEMORY.md alongside any adjacent fenced content captured by the
1764+ // same window. The guard treats marker lines as inside-fence so those
1765+ // ranges are rejected. (#80613)
1766+ it ( "returns true when the range ends on a Light Sleep start marker" , ( ) => {
1767+ const lines = [
1768+ "## Plan" ,
1769+ "- Plan switches use exRule, not abConfig" ,
1770+ "" ,
1771+ "## Light Sleep" ,
1772+ "<!-- openclaw:dreaming:light:start -->" ,
1773+ "- Candidate: staged dream" ,
1774+ "<!-- openclaw:dreaming:light:end -->" ,
1775+ ] ;
1776+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 2 , 5 ) ) . toBe ( true ) ;
1777+ } ) ;
1778+
1779+ it ( "returns true when the range begins on a Light Sleep end marker" , ( ) => {
1780+ const lines = [
1781+ "<!-- openclaw:dreaming:light:start -->" ,
1782+ "- Candidate: staged dream" ,
1783+ "<!-- openclaw:dreaming:light:end -->" ,
1784+ "- normal durable bullet" ,
1785+ ] ;
1786+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 4 ) ) . toBe ( true ) ;
1787+ } ) ;
1788+
1789+ it ( "returns true when the range covers only a marker line" , ( ) => {
1790+ const lines = [
1791+ "<!-- openclaw:dreaming:light:start -->" ,
1792+ "- Candidate: staged dream" ,
1793+ "<!-- openclaw:dreaming:light:end -->" ,
1794+ ] ;
1795+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 1 , 1 ) ) . toBe ( true ) ;
1796+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 3 ) ) . toBe ( true ) ;
1797+ } ) ;
1798+
1799+ it ( "returns true for REM marker single-line ranges even with no body between markers" , ( ) => {
1800+ const lines = [
1801+ "real line 1" ,
1802+ "<!-- openclaw:dreaming:rem:start -->" ,
1803+ "<!-- openclaw:dreaming:rem:end -->" ,
1804+ "real line 4" ,
1805+ ] ;
1806+ // No content between the markers, but the marker text itself must not
1807+ // ride along into a promoted snippet.
1808+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 2 , 2 ) ) . toBe ( true ) ;
1809+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 3 ) ) . toBe ( true ) ;
1810+ // Real-content single lines remain unflagged.
1811+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 1 , 1 ) ) . toBe ( false ) ;
1812+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 4 , 4 ) ) . toBe ( false ) ;
1813+ } ) ;
1814+ } ) ;
1815+
1816+ it ( "does not promote rehydrated candidates whose relocated range covers a managed dreaming fence marker line (#80613)" , async ( ) => {
1817+ await withTempWorkspace ( async ( workspaceDir ) => {
1818+ // Daily note: human content + a managed Light Sleep block. The relevant
1819+ // surface is the marker lines (5 and 8), not the fenced content between
1820+ // them. The existing fence-overlap guard already blocks ranges between
1821+ // the markers; this test exercises the residual edge case where the
1822+ // relocated range covers a marker line itself.
1823+ await writeDailyMemoryNote ( workspaceDir , "2026-05-18" , [
1824+ "## Plan" , // 1
1825+ "- Plan switches use exRule, not abConfig" , // 2
1826+ "" , // 3
1827+ "## Light Sleep" , // 4
1828+ "<!-- openclaw:dreaming:light:start -->" , // 5
1829+ "- Candidate: staged dream" , // 6
1830+ " - confidence: 0.95" , // 7
1831+ "<!-- openclaw:dreaming:light:end -->" , // 8
1832+ ] ) ;
1833+
1834+ // Stored recall snippet equals the marker text exactly, so relocate's
1835+ // exact-match path resolves to (5, 5) with the marker as its snippet.
1836+ // The contamination predicate does not flag bare marker text (no
1837+ // Candidate/Reflections + confidence + evidence + status: staged +
1838+ // recalls signature), so the only line of defense is the fence-overlap
1839+ // guard. Pre-patch the guard returns false for a marker-only range and
1840+ // the marker text leaks into MEMORY.md; post-patch the range is rejected.
1841+ await recordShortTermRecalls ( {
1842+ workspaceDir,
1843+ query : "marker-line edge case" ,
1844+ results : [
1845+ {
1846+ path : "memory/2026-05-18.md" ,
1847+ startLine : 5 ,
1848+ endLine : 5 ,
1849+ score : 0.94 ,
1850+ snippet : "<!-- openclaw:dreaming:light:start -->" ,
1851+ source : "memory" ,
1852+ } ,
1853+ ] ,
1854+ } ) ;
1855+
1856+ const ranked = await rankShortTermPromotionCandidates ( {
1857+ workspaceDir,
1858+ minScore : 0 ,
1859+ minRecallCount : 0 ,
1860+ minUniqueQueries : 0 ,
1861+ } ) ;
1862+ const applied = await applyShortTermPromotions ( {
1863+ workspaceDir,
1864+ candidates : ranked ,
1865+ minScore : 0 ,
1866+ minRecallCount : 0 ,
1867+ minUniqueQueries : 0 ,
1868+ } ) ;
1869+
1870+ expect ( applied . applied ) . toBe ( 0 ) ;
1871+ const memoryText = await fs
1872+ . readFile ( path . join ( workspaceDir , "MEMORY.md" ) , "utf-8" )
1873+ . catch ( ( ) => "" ) ;
1874+ expect ( memoryText ) . not . toContain ( "Promoted From Short-Term Memory" ) ;
1875+ expect ( memoryText ) . not . toMatch ( / o p e n c l a w : d r e a m i n g / i) ;
1876+ } ) ;
17591877 } ) ;
17601878
17611879 it ( "refuses to promote rehydrated candidates that land inside a managed dreaming fence" , async ( ) => {
0 commit comments