Problem:
GET /channels/:channel_id/messages returns a bare newest-first array with no has_more. Clients must infer end-of-history, but the inference is unsound: list_api_responses (fluxer_messages/src/shard_impl.rs:610-628) truncates the raw bucket scan to limit BEFORE applying visibility-cutoff and orphan filtering, so a short (or empty) page does not prove the direction is exhausted. Client cost: pointer-consult ladders, provisional tail seals, extra latest-page confirmation fetches, parked-cursor bookkeeping — and user-facing pagination bugs when the heuristics disagree with reality. The around window has the same defect: clients infer has_more_after from the newer = limit/2 quota (around_window_limits), which post-filtering silently violates.
Non-solution: exposing a raw "scan hit limit" bit — it stays true when every remaining row is invisible to the requester (e.g., paging into message_history_cutoff), producing endless client probes.
Proposal: refill pages over visible rows — continue the bucket scan until limit + 1 visible, non-orphan rows or storage exhaustion; return limit rows plus has_more_before/has_more_after (headers or a wrapped response for array compatibility). Short-circuit: a before cursor whose snowflake timestamp is below the requester's cutoff ⇒ has_more_before = false. For around, report both flags computed the same way. Bonus: an explicit target-missing indicator for around (a deleted target currently returns a neighbour window indistinguishable from success; the mobile client works around it with a 6-second timeout).
Note: gateway resume replays only the session event buffer (session_lifecycle.erl handle_resume) — no history backfill — so clients REST-reconcile after long disconnects; truthful has_more makes that cheaper too.