From b886f826224aae5d058ebf4acca710f74d9601d7 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 09:50:40 -0400 Subject: [PATCH] Clamp backward pagination end bound to the event key-order limit Event keys zero-pad seq to six digits, so an exclusive end bound past MAX_EVENT_SEQ formatted as a seven-digit prefix that sorts before real event keys, producing an inverted scan range. This made the newest page come back empty once a run reached MAX_EVENT_SEQ, and let a client supplied before_seq beyond MAX_EVENT_SEQ garble the range. Clamp the bound and treat anything past MAX_EVENT_SEQ as unbounded; no stored sequence exceeds it, so the results are equivalent. Co-Authored-By: Claude Fable 5 --- .../fabro-store/src/slate/run_store.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/components/fabro-store/src/slate/run_store.rs b/lib/components/fabro-store/src/slate/run_store.rs index bf700f96c..36b28ef0b 100644 --- a/lib/components/fabro-store/src/slate/run_store.rs +++ b/lib/components/fabro-store/src/slate/run_store.rs @@ -389,10 +389,15 @@ impl RunDatabase { before_seq: Option, limit: usize, ) -> Result> { + // Event keys zero-pad seq to six digits (see `keys::run_event_key`), + // so an end bound past `MAX_EVENT_SEQ` would format as a seven-digit + // prefix that breaks lexicographic key order. No stored seq exceeds + // `MAX_EVENT_SEQ`, so clamp and treat that bound as unbounded. let end_seq = match before_seq { Some(seq) => u64::from(seq), None => u64::from(self.latest_event_seq().await?) + 1, - }; + } + .min(u64::from(keys::MAX_EVENT_SEQ) + 1); if end_seq <= 1 { return Ok(Vec::new()); } @@ -400,7 +405,9 @@ impl RunDatabase { let window_size = u64::try_from(limit.saturating_add(1)).unwrap_or(u64::MAX); let start_seq = u32::try_from(end_seq.saturating_sub(window_size).max(1)).unwrap_or(u32::MAX); - let end_seq = u32::try_from(end_seq).ok(); + let end_seq = u32::try_from(end_seq) + .ok() + .filter(|end| *end <= keys::MAX_EVENT_SEQ); let mut events = list_events_in_range_with_limit( &self.inner.db, &self.inner.run_id, @@ -1038,6 +1045,53 @@ mod tests { ); } + #[tokio::test] + async fn list_events_before_with_limit_reads_newest_page_at_max_event_seq() { + let run = fresh_run().await; + let run_id = run.run_id(); + run.inner + .event_seq + .as_ref() + .unwrap() + .store(keys::MAX_EVENT_SEQ - 1, Ordering::SeqCst); + run.append_event(&stage_prompt_payload(&run_id, 1, Some("alpha"))) + .await + .unwrap(); + run.append_event(&stage_prompt_payload(&run_id, 2, Some("beta"))) + .await + .unwrap(); + + let events = run.list_events_before_with_limit(None, 2).await.unwrap(); + + let seqs: Vec = events.iter().map(|event| event.seq).collect(); + assert_eq!(seqs, vec![keys::MAX_EVENT_SEQ, keys::MAX_EVENT_SEQ - 1]); + } + + #[tokio::test] + async fn list_events_before_with_limit_clamps_cursor_beyond_max_event_seq() { + let run = fresh_run().await; + let run_id = run.run_id(); + run.inner + .event_seq + .as_ref() + .unwrap() + .store(keys::MAX_EVENT_SEQ - 1, Ordering::SeqCst); + run.append_event(&stage_prompt_payload(&run_id, 1, Some("alpha"))) + .await + .unwrap(); + run.append_event(&stage_prompt_payload(&run_id, 2, Some("beta"))) + .await + .unwrap(); + + let events = run + .list_events_before_with_limit(Some(u32::MAX), 2) + .await + .unwrap(); + + let seqs: Vec = events.iter().map(|event| event.seq).collect(); + assert_eq!(seqs, vec![keys::MAX_EVENT_SEQ, keys::MAX_EVENT_SEQ - 1]); + } + #[tokio::test] async fn append_event_rejects_sequences_beyond_key_order_limit() { let run = fresh_run().await;