Problem
The feed refetch polls the published feed head, so it never sees articles that reach the feed after their publish time — and those are the majority.
ensure_feed (src/app/feeds.rs:248-262) refetches with head_cmd, i.e. cursor: None, whenever the bundle is older than alphai_ttl and the cursor sits at the top. head_cmd calls /api/news/ with no sort, which is the reverse-chronological feed. apply_alphai then diffs the returned uids against feed_seen to render unseen markers (src/app/feeds.rs:180-196).
That works only if new articles always arrive at the top of the published ordering. They do not. An article ingested now can carry a publish timestamp from an hour ago, which places it below the first page and outside every subsequent refetch. Backend measurement when delta polling was designed: 75.8% of rows arrive behind the published watermark, with general news landing a median of ~33 minutes after publication and SEC filings 5 to 9 minutes after. With the default page_size of 10 the window is narrow, so the miss is not an edge case.
Two visible consequences: the feed silently omits articles, and the unseen-marker count under-reports how much is actually new.
Scope note: I traced this from the code and the API's ordering semantics. I have not instrumented a running TUI to measure its actual miss rate, so treat the percentage as the API-side figure it is, not a measured TUI number.
What the API now offers
GET /api/news/?sort=ingested returns rows in the order they became available (ascending). Its cursor is a polling position rather than an end-of-feed marker: next_cursor is always set, and an empty results means caught up. Each item carries original.created_at, the moment AlphaAI received the article, next to time_published.
Both official SDKs picked this up in 0.4.0. The TUI has its own client (src/alphai.rs), so there is nothing to inherit — this is a local change.
Suggested shape
Do not simply switch the head refetch to sort=ingested. That mode is ascending, which is the wrong reading order for the news pane, and paging back through history still needs published.
Better split:
- keep
published for what is displayed and for request_more_articles paging (unchanged);
- add a separate lightweight
ingested poll whose only job is to detect arrivals, merging anything new into the top of the bundle and into the unseen set.
src/alphai.rs:news() needs a sort argument for that; everything else is orchestration in feeds.rs.
Two traps worth knowing before implementing
Cursors are mode-specific and no longer fail quietly. The two sort modes mint separate cursor families. As of 2026-07-25 replaying a cursor into the other mode returns 400, and so does a corrupted cursor, instead of silently restarting at the feed head. So the ingested poll cursor and the published paging cursor must be stored separately and never crossed. The strictness is deliberate: in delta mode a silent restart skips a range of rows that the client cannot detect afterwards, because a legitimate delta page never contains previously-seen rows either, and a head jump returns rows newer than the last seen. Only the server can catch it.
archive_horizon also applies to where a poll resumes. On Free and Basic keys a cursor left unused for longer than the plan's window (30 / 90 days) comes back 403 with extra.reason = "archive_horizon". Realistic here: the TUI is closed for a month and reopens with a stale stored cursor. is_archive_gate already recognizes the shape for paging, but the poll path should treat it as "reprime from scratch", not as an error to surface. Pro has no window.
Problem
The feed refetch polls the published feed head, so it never sees articles that reach the feed after their publish time — and those are the majority.
ensure_feed(src/app/feeds.rs:248-262) refetches withhead_cmd, i.e.cursor: None, whenever the bundle is older thanalphai_ttland the cursor sits at the top.head_cmdcalls/api/news/with nosort, which is the reverse-chronological feed.apply_alphaithen diffs the returned uids againstfeed_seento render unseen markers (src/app/feeds.rs:180-196).That works only if new articles always arrive at the top of the published ordering. They do not. An article ingested now can carry a publish timestamp from an hour ago, which places it below the first page and outside every subsequent refetch. Backend measurement when delta polling was designed: 75.8% of rows arrive behind the published watermark, with general news landing a median of ~33 minutes after publication and SEC filings 5 to 9 minutes after. With the default
page_sizeof 10 the window is narrow, so the miss is not an edge case.Two visible consequences: the feed silently omits articles, and the unseen-marker count under-reports how much is actually new.
Scope note: I traced this from the code and the API's ordering semantics. I have not instrumented a running TUI to measure its actual miss rate, so treat the percentage as the API-side figure it is, not a measured TUI number.
What the API now offers
GET /api/news/?sort=ingestedreturns rows in the order they became available (ascending). Its cursor is a polling position rather than an end-of-feed marker:next_cursoris always set, and an emptyresultsmeans caught up. Each item carriesoriginal.created_at, the moment AlphaAI received the article, next totime_published.Both official SDKs picked this up in 0.4.0. The TUI has its own client (
src/alphai.rs), so there is nothing to inherit — this is a local change.Suggested shape
Do not simply switch the head refetch to
sort=ingested. That mode is ascending, which is the wrong reading order for the news pane, and paging back through history still needspublished.Better split:
publishedfor what is displayed and forrequest_more_articlespaging (unchanged);ingestedpoll whose only job is to detect arrivals, merging anything new into the top of the bundle and into the unseen set.src/alphai.rs:news()needs asortargument for that; everything else is orchestration infeeds.rs.Two traps worth knowing before implementing
Cursors are mode-specific and no longer fail quietly. The two sort modes mint separate cursor families. As of 2026-07-25 replaying a cursor into the other mode returns
400, and so does a corrupted cursor, instead of silently restarting at the feed head. So the ingested poll cursor and the published paging cursor must be stored separately and never crossed. The strictness is deliberate: in delta mode a silent restart skips a range of rows that the client cannot detect afterwards, because a legitimate delta page never contains previously-seen rows either, and a head jump returns rows newer than the last seen. Only the server can catch it.archive_horizonalso applies to where a poll resumes. On Free and Basic keys a cursor left unused for longer than the plan's window (30 / 90 days) comes back403withextra.reason = "archive_horizon". Realistic here: the TUI is closed for a month and reopens with a stale stored cursor.is_archive_gatealready recognizes the shape for paging, but the poll path should treat it as "reprime from scratch", not as an error to surface. Pro has no window.