Hmbown/CodeWhale · error · anyhow::Error

Runtime event replay_limit cannot exceed {MAX_RUNTIME_EVENT_

Error message

Runtime event replay_limit cannot exceed {MAX_RUNTIME_EVENT_REPLAY_TAIL}

What it means

replay_events bounds tail_limit by MAX_RUNTIME_EVENT_REPLAY_TAIL (4096, crates/tui/src/runtime_threads.rs:70) so a replay cannot materialize an unbounded event window; any request above the cap is rejected before the replay worker spawns (crates/tui/src/runtime_threads.rs:6532). Use the since_seq cursor for full history instead of a huge tail.

Source

Thrown at crates/tui/src/runtime_threads.rs:6532

        thread_id: &str,
        offset: u64,
        limit: Option<usize>,
    ) -> Result<(Vec<RuntimeEventRecord>, u64)> {
        let store = self.store.clone();
        let thread_id = thread_id.to_string();
        tokio::task::spawn_blocking(move || store.events_from_offset(&thread_id, offset, limit))
            .await
            .context("Runtime event cursor task failed")?
    }

    pub(crate) async fn replay_events(
        &self,
        thread_id: &str,
        since_seq: Option<u64>,
        tail_limit: Option<usize>,
    ) -> Result<RuntimeEventReplay> {
        if tail_limit.is_some_and(|limit| limit > MAX_RUNTIME_EVENT_REPLAY_TAIL) {
            bail!("Runtime event replay_limit cannot exceed {MAX_RUNTIME_EVENT_REPLAY_TAIL}");
        }
        let (base_tx, base_rx) = oneshot::channel();
        let (batch_tx, batches) = mpsc::channel(2);
        let store = self.store.clone();
        let thread_id = thread_id.to_string();
        tokio::task::spawn_blocking(move || {
            store.publish_event_replay(&thread_id, since_seq, tail_limit, base_tx, batch_tx);
        });
        let base_seq = base_rx
            .await
            .context("Runtime event replay worker ended before initialization")?
            .map_err(anyhow::Error::msg)?;
        Ok(RuntimeEventReplay { base_seq, batches })
    }

    async fn ensure_engine_loaded(&self, thread_hint: &ThreadRecord) -> Result<EngineHandle> {
        {
            let mut active = self.active.lock().await;

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Clamp tail_limit to 4096 (limit.min(MAX_RUNTIME_EVENT_REPLAY_TAIL)) before calling
  2. Page through history with since_seq cursors instead of one large tail
  3. Request only the window you actually render
  4. Compute limit from viewport size, not from total event count

Example fix

// before
let replay = runtime.replay_events(&thread_id, None, Some(total_event_count)).await?;

// after
const MAX_TAIL: usize = 4096;
let tail = Some(total_event_count.min(MAX_TAIL));
let replay = runtime.replay_events(&thread_id, since_seq, tail).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Clamp before calling.
const MAX_RUNTIME_EVENT_REPLAY_TAIL: usize = 4096;
let tail_limit = tail_limit.map(|l| l.min(MAX_RUNTIME_EVENT_REPLAY_TAIL));
let replay = runtime.replay_events(thread_id, since_seq, tail_limit).await?;

Type guard

fn is_valid_tail_limit(limit: Option<usize>) -> bool {
    limit.is_none_or(|l| l <= 4096)
}

Try / catch

match runtime.replay_events(thread_id, since_seq, tail_limit).await {
    Ok(replay) => { /* ... */ }
    Err(err) if err.to_string().contains("replay_limit cannot exceed") => {
        let clamped = tail_limit.map(|l| l.min(4096));
        runtime.replay_events(thread_id, since_seq, clamped).await?;
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Passing tail_limit = Some(10_000) to replay_events; a client 'load entire history' button requesting all events as a tail; porting an older client that assumed unbounded limits; computing limit as total event count read from elsewhere.

Common situations: New UI features requesting big windows; scripts copying a store's event count into replay_limit; version upgrades that introduced the cap after clients were written.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/3d66068e326d9b55. Report an issue: GitHub.