@@ -1198,6 +1198,124 @@ describe("short-term promotion", () => {
11981198 expect ( testing . lineRangeOverlapsDreamingFence ( lines , 8 , 8 ) ) . toBe ( false ) ;
11991199 expect ( testing . lineRangeOverlapsDreamingFence ( lines , 6 , 6 ) ) . toBe ( true ) ;
12001200 } ) ;
1201+
1202+ // Marker lines themselves carry managed-block content. A relocated range
1203+ // that includes a `<!-- openclaw:dreaming:*:start/end -->` marker would
1204+ // build its snippet from raw lines that contain that marker text, leaking
1205+ // it into MEMORY.md alongside any adjacent fenced content captured by the
1206+ // same window. The guard treats marker lines as inside-fence so those
1207+ // ranges are rejected. (#80613)
1208+ it ( "returns true when the range ends on a Light Sleep start marker" , ( ) => {
1209+ const lines = [
1210+ "## Plan" ,
1211+ "- Plan switches use exRule, not abConfig" ,
1212+ "" ,
1213+ "## Light Sleep" ,
1214+ "<!-- openclaw:dreaming:light:start -->" ,
1215+ "- Candidate: staged dream" ,
1216+ "<!-- openclaw:dreaming:light:end -->" ,
1217+ ] ;
1218+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 2 , 5 ) ) . toBe ( true ) ;
1219+ } ) ;
1220+
1221+ it ( "returns true when the range begins on a Light Sleep end marker" , ( ) => {
1222+ const lines = [
1223+ "<!-- openclaw:dreaming:light:start -->" ,
1224+ "- Candidate: staged dream" ,
1225+ "<!-- openclaw:dreaming:light:end -->" ,
1226+ "- normal durable bullet" ,
1227+ ] ;
1228+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 4 ) ) . toBe ( true ) ;
1229+ } ) ;
1230+
1231+ it ( "returns true when the range covers only a marker line" , ( ) => {
1232+ const lines = [
1233+ "<!-- openclaw:dreaming:light:start -->" ,
1234+ "- Candidate: staged dream" ,
1235+ "<!-- openclaw:dreaming:light:end -->" ,
1236+ ] ;
1237+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 1 , 1 ) ) . toBe ( true ) ;
1238+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 3 ) ) . toBe ( true ) ;
1239+ } ) ;
1240+
1241+ it ( "returns true for REM marker single-line ranges even with no body between markers" , ( ) => {
1242+ const lines = [
1243+ "real line 1" ,
1244+ "<!-- openclaw:dreaming:rem:start -->" ,
1245+ "<!-- openclaw:dreaming:rem:end -->" ,
1246+ "real line 4" ,
1247+ ] ;
1248+ // No content between the markers, but the marker text itself must not
1249+ // ride along into a promoted snippet.
1250+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 2 , 2 ) ) . toBe ( true ) ;
1251+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 3 , 3 ) ) . toBe ( true ) ;
1252+ // Real-content single lines remain unflagged.
1253+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 1 , 1 ) ) . toBe ( false ) ;
1254+ expect ( testing . lineRangeOverlapsDreamingFence ( lines , 4 , 4 ) ) . toBe ( false ) ;
1255+ } ) ;
1256+ } ) ;
1257+
1258+ it ( "does not promote rehydrated candidates whose relocated range covers a managed dreaming fence marker line (#80613)" , async ( ) => {
1259+ await withTempWorkspace ( async ( workspaceDir ) => {
1260+ // Daily note: human content + a managed Light Sleep block. The relevant
1261+ // surface is the marker lines (5 and 8), not the fenced content between
1262+ // them. The existing fence-overlap guard already blocks ranges between
1263+ // the markers; this test exercises the residual edge case where the
1264+ // relocated range covers a marker line itself.
1265+ await writeDailyMemoryNote ( workspaceDir , "2026-05-18" , [
1266+ "## Plan" , // 1
1267+ "- Plan switches use exRule, not abConfig" , // 2
1268+ "" , // 3
1269+ "## Light Sleep" , // 4
1270+ "<!-- openclaw:dreaming:light:start -->" , // 5
1271+ "- Candidate: staged dream" , // 6
1272+ " - confidence: 0.95" , // 7
1273+ "<!-- openclaw:dreaming:light:end -->" , // 8
1274+ ] ) ;
1275+
1276+ // Stored recall snippet equals the marker text exactly, so relocate's
1277+ // exact-match path resolves to (5, 5) with the marker as its snippet.
1278+ // The contamination predicate does not flag bare marker text (no
1279+ // Candidate/Reflections + confidence + evidence + status: staged +
1280+ // recalls signature), so the only line of defense is the fence-overlap
1281+ // guard. Pre-patch the guard returns false for a marker-only range and
1282+ // the marker text leaks into MEMORY.md; post-patch the range is rejected.
1283+ await recordShortTermRecalls ( {
1284+ workspaceDir,
1285+ query : "marker-line edge case" ,
1286+ results : [
1287+ {
1288+ path : "memory/2026-05-18.md" ,
1289+ startLine : 5 ,
1290+ endLine : 5 ,
1291+ score : 0.94 ,
1292+ snippet : "<!-- openclaw:dreaming:light:start -->" ,
1293+ source : "memory" ,
1294+ } ,
1295+ ] ,
1296+ } ) ;
1297+
1298+ const ranked = await rankShortTermPromotionCandidates ( {
1299+ workspaceDir,
1300+ minScore : 0 ,
1301+ minRecallCount : 0 ,
1302+ minUniqueQueries : 0 ,
1303+ } ) ;
1304+ const applied = await applyShortTermPromotions ( {
1305+ workspaceDir,
1306+ candidates : ranked ,
1307+ minScore : 0 ,
1308+ minRecallCount : 0 ,
1309+ minUniqueQueries : 0 ,
1310+ } ) ;
1311+
1312+ expect ( applied . applied ) . toBe ( 0 ) ;
1313+ const memoryText = await fs
1314+ . readFile ( path . join ( workspaceDir , "MEMORY.md" ) , "utf-8" )
1315+ . catch ( ( ) => "" ) ;
1316+ expect ( memoryText ) . not . toContain ( "Promoted From Short-Term Memory" ) ;
1317+ expect ( memoryText ) . not . toMatch ( / o p e n c l a w : d r e a m i n g / i) ;
1318+ } ) ;
12011319 } ) ;
12021320
12031321 it ( "refuses to promote rehydrated candidates that land inside a managed dreaming fence" , async ( ) => {
0 commit comments