Summary
listSignals({ status, since }) filters by s.created_at > ? in the DO route, but two open PRs consume the result to compute reviewed_at-derived metrics. This produces silently-wrong values when reviews happen on signals created outside the window — exactly the case DRIs / contributors most want to see.
Verified evidence
The DO /signals handler at src/objects/news-do.ts:2570-2632 calls buildSignalListWhere({ since }) which at src/objects/news-do.ts:123-125 adds:
if (filters.since) {
clauses.push("s.created_at > ?");
params.push(filters.since);
}
So since is a created_at lower-bound, not a reviewed_at lower-bound.
Affected callers
Two callers fetch listSignals({ status: 'approved'|'rejected', since, ... }) and consume reviewed_at from the results:
1. PR #712 — lastReviewedAt for beat health snapshot
src/routes/world-model.ts:43 (commit a824f28):
const [counts, approvedSignals, rejectedSignals] = await Promise.all([
getSignalCounts(c.env, { beat: beat.slug, since }),
listSignals(c.env, { beat: beat.slug, status: "approved", since, limit: 50 }),
listSignals(c.env, { beat: beat.slug, status: "rejected", since, limit: 50 }),
]);
// ...
{ ...row.editor, lastReviewedAt: latestReviewedAt([...approvedSignals, ...rejectedSignals]) }
lastReviewedAt is the most recent reviewed_at from the result. But the result excludes signals created before since, even if they were reviewed inside the window. A 7-day-old submitted signal reviewed today is invisible to a ?since=24h-ago query — even though that review is exactly the editorial activity DRIs want to see.
2. PR #713 — reviewedInWindow for queue velocity estimate
src/lib/review-queue.ts:40-50 (commit 53bdcd8):
const [approvedRecent, rejectedRecent] = await Promise.all([
listSignals(env, { beat, status: "approved", since, limit: 500 }),
listSignals(env, { beat, status: "rejected", since, limit: 500 }),
]);
const reviewedInWindow = approvedRecent.length + rejectedRecent.length;
The result feeds estimateReviewTime(queuePosition, reviewedInWindow) — the user-facing wait estimate. A beat with 0 new signals in the last 24h but 50 reviews of older backlogged signals reads as reviewedInWindow = 0, velocity 0, estimate null. The endpoint then returns estimated_review_time: null for everyone in queue when in reality the editor was extremely active.
Concrete failure mode
Reproduces today (2026-05-07T21:30Z) on prod assuming the quantum beat has any pre-since submitted-then-reviewed-recently signals:
# Velocity reads as low...
curl -s "https://aibtc.news/api/world-model/beat-health?since=2026-05-06T21:30:00Z" \
| jq '.beats[] | select(.slug == "quantum") | .editor.lastReviewedAt'
# ... but the editor actually reviewed signals during the window. Their reviews are
# only visible if you also fetch by reviewed_at.
Fix options
Two non-equivalent paths:
-
Add a reviewed_since filter to SignalFilters — separate field, separate WHERE clause (s.reviewed_at > ?), keep since as created_at filter. Callers that want recent-reviews use reviewed_since; callers that want recent-submissions use since. Backward-compatible with all current call sites.
-
Rename since semantically by call site — e.g., extract a listReviewedSignals(env, { beat, status, reviewedSince, limit }) helper that calls the same DO route but builds a different WHERE clause server-side. Requires the DO route to accept a new query param (reviewed_since) and route to the right column.
(1) is mechanically simpler — one new field, one new clause. (2) is more intent-revealing at call sites but doubles the helper surface area.
Either fix unblocks #712's lastReviewedAt and #713's reviewedInWindow consistently, with a single resolution.
Why file this separately
Both PRs hit the same upstream surface; resolving in either PR thread risks the other PR landing inconsistently. A standalone issue centralizes the resolution and lets either Nuval999 or arc address it once.
Cross-references
🤖 Generated with Claude Code
Summary
listSignals({ status, since })filters bys.created_at > ?in the DO route, but two open PRs consume the result to computereviewed_at-derived metrics. This produces silently-wrong values when reviews happen on signals created outside the window — exactly the case DRIs / contributors most want to see.Verified evidence
The DO
/signalshandler atsrc/objects/news-do.ts:2570-2632callsbuildSignalListWhere({ since })which atsrc/objects/news-do.ts:123-125adds:So
sinceis acreated_atlower-bound, not areviewed_atlower-bound.Affected callers
Two callers fetch
listSignals({ status: 'approved'|'rejected', since, ... })and consumereviewed_atfrom the results:1. PR #712 —
lastReviewedAtfor beat health snapshotsrc/routes/world-model.ts:43(commita824f28):lastReviewedAtis the most recentreviewed_atfrom the result. But the result excludes signals created beforesince, even if they were reviewed inside the window. A 7-day-old submitted signal reviewed today is invisible to a?since=24h-agoquery — even though that review is exactly the editorial activity DRIs want to see.2. PR #713 —
reviewedInWindowfor queue velocity estimatesrc/lib/review-queue.ts:40-50(commit53bdcd8):The result feeds
estimateReviewTime(queuePosition, reviewedInWindow)— the user-facing wait estimate. A beat with 0 new signals in the last 24h but 50 reviews of older backlogged signals reads asreviewedInWindow = 0, velocity 0, estimatenull. The endpoint then returnsestimated_review_time: nullfor everyone in queue when in reality the editor was extremely active.Concrete failure mode
Reproduces today (2026-05-07T21:30Z) on prod assuming the
quantumbeat has any pre-sincesubmitted-then-reviewed-recently signals:Fix options
Two non-equivalent paths:
Add a
reviewed_sincefilter toSignalFilters— separate field, separate WHERE clause (s.reviewed_at > ?), keepsinceascreated_atfilter. Callers that want recent-reviews usereviewed_since; callers that want recent-submissions usesince. Backward-compatible with all current call sites.Rename
sincesemantically by call site — e.g., extract alistReviewedSignals(env, { beat, status, reviewedSince, limit })helper that calls the same DO route but builds a different WHERE clause server-side. Requires the DO route to accept a new query param (reviewed_since) and route to the right column.(1) is mechanically simpler — one new field, one new clause. (2) is more intent-revealing at call sites but doubles the helper surface area.
Either fix unblocks #712's
lastReviewedAtand #713'sreviewedInWindowconsistently, with a single resolution.Why file this separately
Both PRs hit the same upstream surface; resolving in either PR thread risks the other PR landing inconsistently. A standalone issue centralizes the resolution and lets either Nuval999 or arc address it once.
Cross-references
lastReviewedAtreviewedInWindow🤖 Generated with Claude Code