B
Briefed

Why your GPT newsletter gets repetitive (and how to fix it)

6 min read · Last updated July 8, 2026

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:

seen_store.py - dedupe memory plus diversity filter
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:

Negative-context prompt section
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.

Don't want to maintain this?

Briefed does the same thing - a personalized, AI-written newsletter on your schedule - free, in about 30 seconds, with deliverability and refinement handled for you. See the full DIY vs Briefed comparison for honest numbers.

No credit card. No app to install. Unsubscribe with one click, anytime.

Questions

Would switching to a bigger model fix the repetitiveness?

Mostly no. Repetition comes from the pipeline - stateless runs, static prompts, shrinking sources - not from model capability. A stronger model writes better individual summaries but will still re-cover last week's story if nothing tells it that story was already sent.

How many items should I actually pass to the LLM per issue?

For a readable digest, curate down to roughly 8-15 items before the LLM call. Score and filter with cheap code (recency, dedupe, per-source caps) and spend the model's attention on the survivors. Fifty raw articles in one prompt reliably produces skimmed, anchored output.

Is embedding-based deduplication worth it over title hashing?

As a second layer, yes. Title-plus-domain hashing catches exact and near-exact repeats cheaply. Embedding similarity additionally catches the same story written up by different outlets. Start with hashing - it removes most repetition for a few lines of code - and add embeddings only if cross-outlet duplicates remain a problem.

How often should I review or rewrite my digest prompt?

Monthly is a reasonable cadence for a personal digest. Read the last several issues in one sitting, note repeated phrasings and structural sameness, and adjust the variable parts of the prompt. If you cannot sustain that habit, that is a signal the maintenance cost exceeds what the pipeline gives you.

More guides