The first week of a self-built GPT newsletter is great. By week four, every issue reads the same, the same stories keep resurfacing, and the summaries all have that identical polished-but-empty register. This degradation is not the model getting worse - it is a set of predictable pipeline failures, and each one has a concrete fix.
The five failure modes
1. Static prompt, shifting news
You wrote one prompt in week one and never touched it. The news changes daily but the framing, section structure, tone instructions and emphasis are frozen. The model dutifully pours different content into an identical mold, so every issue feels the same even when the underlying stories differ. Worse, prompts optimized against week-one news often mis-fit later news - a prompt tuned while a big story dominated will keep hunting for that story's shape.
2. No dedupe memory across issues
Most DIY pipelines are stateless: fetch feeds, summarize, send, forget. A slow-moving story that stays in the RSS window for a week gets summarized five times. Follow-up articles about last Tuesday's announcement look new to a pipeline with no memory. Readers notice repetition long before you do, because you skim your own output.
3. Summarizer collapse
Ask an LLM to summarize 10 items in one call with one instruction and you get 10 summaries in one register: same sentence rhythm, same hedging, same significance-signaling phrases. A funding round, a research paper and a security incident all come out sounding like the same press release. The information survives; the texture that makes a digest readable dies.
4. Source rot
Feeds die silently. A blog moves domains, a feed URL 404s, a site adds bot protection that returns an empty page. Your pipeline does not crash - it just quietly works with 6 sources instead of 12, and the surviving sources dominate. Three months in, your digest is effectively a summary of two websites and you never got an error.
5. Context stuffing
Dumping 50 full articles into one prompt feels thorough but produces worse output than a curated 10. With overloaded context the model skims: it anchors on items early in the prompt, favors items with familiar framing, and drops the odd-but-interesting outliers - exactly the items a good digest exists to surface.
Fixes that actually work
Keep a seen-items store and filter before the LLM
The single highest-leverage fix is a persistent store of what you already sent, plus a pre-LLM scoring pass for recency and source diversity. This keeps repeats out and stops one prolific source from crowding out the rest. A minimal SQLite implementation:
import hashlib
import sqlite3
import time
DB_PATH = "digest_state.db"
SEEN_WINDOW_DAYS = 14
MAX_PER_SOURCE = 3
MAX_TOTAL = 12
def _connect():
conn = sqlite3.connect(DB_PATH)
conn.execute(
"CREATE TABLE IF NOT EXISTS seen ("
"item_hash TEXT PRIMARY KEY, url TEXT, sent_at REAL)"
)
return conn
def item_hash(item):
# Hash normalized title + domain, not the URL: the same story
# often reappears under tracking-parameter variants.
key = item["title"].lower().strip() + "|" + item["domain"]
return hashlib.sha256(key.encode("utf-8")).hexdigest()
def filter_items(candidates):
conn = _connect()
cutoff = time.time() - SEEN_WINDOW_DAYS * 86400
seen = {
row[0]
for row in conn.execute(
"SELECT item_hash FROM seen WHERE sent_at > ?", (cutoff,)
)
}
fresh = [c for c in candidates if item_hash(c) not in seen]
# Score: newer is better, small boost for underrepresented sources.
fresh.sort(key=lambda c: c["published_ts"], reverse=True)
picked = []
per_source = {}
for c in fresh:
n = per_source.get(c["domain"], 0)
if n >= MAX_PER_SOURCE:
continue
per_source[c["domain"]] = n + 1
picked.append(c)
if len(picked) >= MAX_TOTAL:
break
return picked
def mark_sent(items):
conn = _connect()
now = time.time()
conn.executemany(
"INSERT OR REPLACE INTO seen VALUES (?, ?, ?)",
[(item_hash(i), i["url"], now) for i in items],
)
conn.commit()Two details matter. Hash the normalized title plus domain rather than the raw URL, because syndicated and repeatedly-shared stories arrive under many URLs. And cap items per source before capping the total, so diversity is enforced structurally instead of hoped for.
Pass recent issues as negative context
Dedupe catches exact repeats; it does not catch the model re-explaining the same background every issue. Store the last 2-3 issue summaries and include them in the prompt with an explicit instruction:
Here are summaries of the last 3 issues you wrote:
<recent_issues>...</recent_issues>
Rules:
- Do not re-explain background already covered in a recent issue.
- If a story is a follow-up to one covered recently, lead with what is NEW
and reference the earlier coverage in one clause.
- Skip any item whose substance was already covered, even if the article is new.Rotate and parametrize prompt sections
Break the prompt into fixed parts (output format, safety rules) and variable parts (tone notes, section emphasis, one editorial question to answer). Rotate the variable parts on a schedule or randomize per issue. Even a small pool of 4-5 angle instructions - what changed this week, what is overhyped, what did smaller outlets cover that big ones missed - visibly breaks the monotone. Against summarizer collapse specifically, summarize items in separate calls or instruct per-category registers: incidents get factual and terse, analysis pieces get one sentence of why-it-matters, releases get concrete changes only.
Monitor your sources like production dependencies
Log the item count per source per run and alert when a source returns zero items for 3 consecutive runs or the total pool drops below a floor. This is five lines in your fetch loop and it is the difference between noticing source rot in a week versus discovering in month three that your digest has been two blogs in a trench coat.
Schedule prompt reviews - and face the feedback problem
Put a monthly 20-minute review on the calendar: read the last 4 issues back to back and ask what a stranger would find repetitive. Reading them in sequence exposes patterns invisible issue by issue. But this is also where DIY pipelines hit their structural limit: you are the builder, the operator and the only reader, so the only feedback signal is your own attention - which fades exactly when the output gets samey. Real newsletters improve through reader feedback loops. A single-user pipeline has none, and no amount of tooling fully substitutes for a second pair of eyes telling you an issue missed the point.
How Briefed handles this
Briefed is a free personalized newsletter service built around the two things a DIY pipeline lacks. First, freshness by construction: each issue is written individually per subscriber by Claude from public web sources, working from your free-text interest description rather than a frozen prompt-and-feed-list, on the schedule you pick (daily, every 2 days or weekly). Second, a built-in human feedback loop: you refine your newsletter by replying to any issue in plain language - too much of X, go deeper on Y, drop the intros - and subsequent issues adjust. That reply channel is exactly the feedback signal a solo pipeline cannot give itself.
It is 100 percent free with no paid tier, ads or card required, uses double opt-in and one-click unsubscribe, and sends from its own authenticated infrastructure - so the deliverability problems from the companion article never become yours either.