BTW, I am still looking for a reason for the hard-prune logic to live. It seems to complicate matters far more than it's worth --- in particular the way that the WAL replay representation is set up seems confusing and fragile. (If prune_hard is set, the "redirect" entries mean something completely different.) There was some suggestion that VACUUM FULL has to have it, but unless I see proof of that I'm thinking of taking it out.
The notion of hard prune has changed since we recently decided to always prune the chain upto and including the latest DEAD tuple (which includes the preceding RECENTLY_DEAD tuples). Earlier hard_prune involved pruning upto the latest DEAD tuple. The only extra thing hard_prune now does is that it clears the redirected line pointers (I hope I have fixed the comments to reflect this change; but my apologies if I haven't)
But that seems like a worthy thing to do to me. One because thats the cheapest (and may be the easiest) way of getting rid of redirected line pointers and two because that helps us the book-keeping in VACUUM FULL. You are already finding that complex - with the redirected line pointers it might be even more complex.
If you are worried about the WAL part, may be we can fix that some other way.