Repository navigation
The news watcher, and conditional GET for the feeds it polls #23
Closed
robert-dean
started this conversation in
Ideas
Replies: 2 comments
|
Status check, 2026-09-22 (main at 23b983e) §1 (the bulletin) and §4 (the console page) remain built. §2 is not: nothing polls a source and posts |
0 replies
|
Built, 2026-10-28, in #810. Both open halves are done.
Still undecided, and not part of this: whether a bulletin names its sources out loud. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Status (2026-10-10)
The source half, the bulletin and the console page are built. This discussion is now about the
two pieces that are not. What shipped, so nothing below has to re-derive it:
feed.parse.tsin the plugin SDK (parseFeed,fetchFeed), anewscapability(
capabilities/news.ts:listFeeds(),fetchItems({ feedId?, limit, since? })), the bundledplugins/rss, afromConfigallowlist entry that resolves a LIST of hostnames, andmodules/newswithGET /news/feedsandGET /news.read_news(modules/llm/news.tool.ts), so a model can say something true about the worldin an ordinary talk break.
newssegment kind,ModelNewsBreakWriterin front ofNewsBreakWriterasthe floor, fed once through
BulletinSource(director/bulletin.source.ts). Both writers declinewhen there is nothing to report. Freshness is judged against when the break airs. Categories
(
deadair.topics,news.classify.ts) let a band on the format clock ask for one subject, and acategory can be kept off air. Since then: whole-sentence news and a leaner news voice (Breaks: settings for the break model, whole-sentence news, cleaner titles, a leaner news voice #369), a
newsreader who reads bulletins in their own character and voice whoever is hosting (A newsreader: bulletins read in their own character and voice, whoever is hosting #415), feeds
that no longer queue behind one another (Stop RSS feeds queueing behind one another in the test and in bulletins #327), and pictures for a bulletin (A weather forecast and a news bulletin have pictures of their own #171).
apps/web/src/components/news/news.page.tsx.One question about the built half is still undecided and is not part of either piece below:
whether a bulletin names its sources out loud.
BreakStory.sourcecarries the feed(
director/break.writer.ts) and neither news writer reads it, because attribution is a station's own decision and a model shown apublisher's name will credit it in a sentence nobody asked for.
What is left: §2, the watcher that would put breaking news on air, and §3, conditional GET on the
host. Neither exists on 2026-10-10: nothing polls a source and posts a
BreakRequest, andpackages/plugin-sdk/src/feed.parse.ts:26still says ETag and Last-Modified are not there. (Theconditional.get.middleware.tsinapps/apiis the station answering ITS clients with a 304, whichis the other direction.)
The contract both pieces stand on
The de-duplication contract is the piece to hold onto. A
NewsItem.idis stable for the sameentry across calls (
guid → id → link → hash, qualified by feed),publishedAtis ISO-8601, andNewsQuery.sinceasks what is new. The RSS plugin's cache is a floor on how often a publisher isasked and never a filter on what comes back, so a caller polling faster sees repeats (which its ids
handle) rather than gaps (which they do not). Everything below is buildable in one pass BECAUSE of
that; a source that could not answer "what is new since I last looked" without holding state per
caller is the version of this that would have to be redesigned first.
2. Breaking news, and it is NOT a news feature
The thing worth building is a small host-side watcher over a polled capability: a loop that asks
a source what is new since it last looked and posts a
BreakRequest(kind, urgency,context,key+cooldownMs) toDirectorService, like every other producer.News is its first subject and not its only one. A chart with a new number one, a scrobble milestone
and a weather warning are the same shape, which is why this is a watcher over a source rather than
a
NewsWatch.Three rules, and all three are already paid for:
and it must stay that way (
docs/internals/director.md§ "Who owns the running order": everywriter posts a command and none of them writes the running order). Polling is host code, so
urgency, cooldown and whether the station is even on air stay host decisions.
publishedAt/sincecontractabove, plus a cache that never filters. A watcher that had to hold per-plugin state to tell a
repeat from an arrival is the hard version of this.
categoriesand the story's own words are what a publishersupplies; whether something is worth interrupting a record for is the watcher's judgement over
operator settings, not a boolean a feed hands us. A field for it would be the plugin API claiming
to know something it cannot.
Two things to get right when it lands.
BreakUrgency(director/break.request.ts:43:'interrupt' | 'next' | 'soon' | 'whenever') already splits on the thing that matters: aninterruptornextrequest is RENDERED before it is injected, which is exactly right here,because a bulletin that arrives at its slot unready is a moment that does not come round again. And
the watcher needs a
pollHintMsfloor and a per-key cooldown, or a publisher that re-dates itsfront page will have the station announcing the same story every four minutes.
Where it would sit, checked 2026-10-10:
director/welcome.announcer.tsis the template, and saysso. It is the first producer on
StationBus, and its header comment describes this watcher as"this class with two words changed": a poller publishing
news.receivedand a subscriber turning itinto
{ kind: 'news', urgency: 'interrupt', context: { headline } }, neither knowing the otherexists. Whether that is the bus's narrow exception or a plain job posting directly is
apps/api/CLAUDE.md§ "How work is dispatched"'s call, and it turns on where the poller registersin
modules.tsrelative todirector. TheBreakRequest.sourcefield is "for the log and the activity feed, never for a decision", so awatcher cannot buy priority by naming itself; it has to earn it through kind and urgency like
everything else.
3. Host-side conditional GET
ETag/Last-Modified, so a poll that finds nothing new costs a 304 rather than a document. Theright home once there is a second feed-reading plugin, or once the watcher above is polling on a
schedule, at which point it is the same code either way. With one consumer it would be a host
surface built for an audience of one; the RSS plugin's in-memory window covers today's actual
failure mode, which is a model calling the tool twice inside one break.
The second consumer has arrived, which moves this up.
plugins/podcastreads the shows thestation carries from their feeds, through the same
feed.parse.ts, so the "audience of one" argumentno longer holds on its own. It is still cheapest to land WITH the watcher, because a watcher polling
on a schedule is the caller that makes an unconditional fetch expensive.
Related
toolcapability this was expected to arrive behind,and why it did not need to.
second kind of break inherits for nothing.
and the watcher's obvious second subject.
Moved from
docs/todo/news-and-bulletins.md. The file as it last stood: https://github.com/robert-dean/deadair/blob/71e431d45ce7b35a189c7d226832ec4a6247e9dc/docs/todo/news-and-bulletins.md. Rewritten 2026-10-10 down to what is still open; the full earlier text, including the bulletin's design notes, is in this discussion's edit history and indocs/internals/breaks.md.All reactions