When a ferry sinks or a central bank moves rates, thirty outlets publish within the hour. A normal RSS reader shows you thirty cards. I build Hushread, an RSS and news reader with no algorithm, and it shows that event once: one story, with a timeline of who reported what and when. This is how that grouping works, and where it went wrong on the way. Every number below is from the code as of October 2026.

Step 1: pull every feed fairly

Feeds wait in one queue ordered by when they were last pulled, so the feed that has waited longest goes next and a few busy sources can't hog the loop. Feeds people follow are pulled every 15 minutes. A source can be an RSS or Atom feed, a site without a feed, a subreddit, a YouTube channel or a public Facebook page; they all land in the same queue.

Each article is opened and cleaned to reader view (Mozilla Readability for the body, Defuddle for metadata and as the fallback) and dated from the feed or the page. If neither has a date, the date stays blank. Stamping "now" on an undated article makes old news look new, and grouping by time would then join things that never happened together.

Step 2: link each new article to an earlier one, not to a cluster

Once a minute, new articles are embedded in one batched call. Each article then links to at most one earlier article from the previous 12 hours, when the two are close enough (cosine similarity 0.92 by default). That gives chains: article 12 points to 10, 10 points to 7. A chain is an event.

Chains beat re-clustering for one reason: they are cheap to keep correct while articles keep arriving. Each new article makes exactly one decision, and nothing already linked has to move.

Step 3: guard the joins, because one bridging article fuses two events

The bug that taught me this: in October 2026 one article, "Netanyahu invites the pilot", sat close to two stories at once. It pulled a 90-article story about a Netanyahu visit to the UAE into a hijacking story. The two stories' centres were only 0.66 alike, while real fragments of the hijacking story sat at 0.73 to 0.86.

The fix was a rule, not a bigger model. One article may merge two stories only when the smaller one is still a fragment (8 articles or fewer, such as early or other-language copies) or when the two stories' centres agree (0.72 or more). Otherwise the article joins one story and leaves the other alone.

Step 4: count publishers, not articles

"Thirty articles" can be one outlet syndicated thirty times. A publisher is a normalised outlet name plus its domain, and a story's weight is the number of distinct publishers in it. Trending ranks stories by that count, never by clicks, and a story only trends once 5 or more different publishers cover it.

Step 5: write only after corroboration

A written Story appears only when an event clears one of two bars:

  • Burst: 8 or more publishers, and a busiest hour with at least 4 new reports.
  • Breadth: 15 or more publishers over several hours, with a busiest hour of at least 3. This second path exists because coverage centred in one time zone (India, for example) rarely puts four outlets in a single hour.

Stories are written by an AI model from those outlets' reporting alone, and the page says so. Claims are attributed to the outlets that made them, every timeline entry links to the original report, and the byline is "Hushread Stories" with the number of outlets the piece was written from, because no person wrote it. While coverage continues, a Story takes a dated update when at least 2 new articles have arrived, at most every 6 hours.

Step 6: close stories, then chain them into threads

A story closes after 48 quiet hours or at 7 days old. A closed story is linked to the one it follows when their centres are 0.78 alike or more and it still resembles the thread's first story (0.70). So "the next round of sanctions" joins the sanctions thread, while one football match doesn't swallow the next. Article, story, thread: the event, then the saga. Long stories show up as chapters, each with its own summary.

What I'd tell anyone building this

  • Make every threshold an environment variable, and give every stage a kill switch. I tuned these on a 14-day backfill, and some of them twice.
  • Write down the bug that set each threshold right next to the number. The comment above the 0.72 in my code is the hijacking story.
  • Trust publisher counts over article counts, and corroboration over speed.