An earlier post here mentioned, in passing, that RSS’s seen-article tracking “had a real resurfacing risk if a long-idle feed’s window rotated all the way through.” That’s a strange-sounding failure mode worth actually explaining, because the fix for it is a good example of a gap that looks small until you trace through exactly when it bites.

PIM tracks which articles in a feed you’ve already been shown by keeping a “seen” set of article ids per feed. The original version pruned that set on every fetch down to only the ids still present in the feed’s current window — reasonable on the surface, since a feed’s own published list eventually drops old items anyway, so why keep tracking ids for articles that aren’t even being served anymore. The problem: if a feed goes quiet long enough that its whole current window rotates past everything that was in your seen set, the prune wipes the set to nothing, with no memory that any of those articles were ever shown. If that same feed later republishes an old article — or backdates one, which happens more often than you’d expect — with the same id it had the first time, PIM has no record it was ever seen, and it comes back as if it were new.

The fix replaces window-based pruning with age-based pruning: each article id gets its own “first seen” timestamp, and ids only drop out of the set once they’re genuinely old — 180 days by default — regardless of whether the feed’s current window still mentions them. A file upgrade path matters here too: an existing seen-file with no timestamps at all gets read as “seen just now” rather than dropped, so turning this on doesn’t reset anyone’s actual read history.

A second, smaller fix landed the same day: the database table backing Saved articles had no real uniqueness guarantee on an article’s id, so a rare write-side race could leave two rows for what’s really one article. Rather than add a hard database constraint — risky on a schema with no migration tool, since any install that already has a duplicate would fail to even start up — the read and write paths both got taught to de-duplicate and clean up after themselves instead, landing at the same practical guarantee without the startup-crash risk.

Three other flagged items from the original comparison turned out, on a second look, not to be real gaps at all: keeping the seen-set in a local file instead of the database matches a deliberate, established convention this codebase already uses for exactly this kind of lightweight per-machine state; marking an article “seen” the moment it’s shown rather than only once fully read is standard behavior for Feedbin, NewsBlur, and Miniflux too, not something PIM does worse; and the remaining fallback-identity edge case (a republished article with a slightly edited title) has no fix that wouldn’t also break a legitimately-updated one — an industry-standard limitation, not a PIM-specific shortcoming. Not every item on a gap list survives a second look, and it’s worth saying so instead of quietly fixing everything a first pass flagged.