tinyhumansai/openhuman · error

[agent] replay read_run_events_page failed run_id={run_id}:

Error message

[agent] replay read_run_events_page failed run_id={run_id}: {e}

What it means

Replay pagination failed while reading the durable event journal: StoreEventJournal::read_from(run_id, offset) errored (replay/ops.rs:67). The page size is already clamped into [1, MAX_EVENTS_LIMIT] before the read, so the failure is storage-level — opening or reading the sqlite-backed journal in the workspace session stores (missing/moved workspace, file lock, corruption, permissions).

Source

Thrown at src/openhuman/agent/tinyagents/replay/ops.rs:67

pub(crate) async fn read_run_events_page(
    workspace: &Path,
    run_id: &str,
    offset: u64,
    limit: u64,
) -> anyhow::Result<RunEventsPage> {
    // Guard the page size: clamp a zero/absurd limit into [1, MAX].
    let effective_limit = limit.clamp(1, MAX_EVENTS_LIMIT);
    log::debug!(
        "[agent] replay read_run_events_page run_id={run_id} offset={offset} \
         limit={limit} effective_limit={effective_limit}"
    );

    let stores = open_session_stores(workspace);
    let journal = StoreEventJournal::new(stores.journal);
    // Read one extra record to detect whether a further page exists without a
    // second store round-trip.
    let mut events = journal.read_from(run_id, offset).await.map_err(|e| {
        anyhow::anyhow!("[agent] replay read_run_events_page failed run_id={run_id}: {e}")
    })?;

    let has_more = events.len() as u64 > effective_limit;
    if has_more {
        events.truncate(effective_limit as usize);
    }
    // Offsets are monotonic within a run, so the cursor is simply "one past the
    // last returned offset". `None` when this page drained the stream.
    let next_offset = if has_more {
        events.last().map(|obs| obs.offset + 1)
    } else {
        None
    };

    log::debug!(
        "[agent] replay read_run_events_page run_id={run_id} returned={} next_offset={:?}",
        events.len(),
        next_offset

View on GitHub (pinned to a221052e0d)

Solutions

  1. Verify the workspace path exists and is writable by this process
  2. Retry after the run that owns the journal finishes writing — sqlite busy locks are transient
  3. Check disk space and filesystem health; a journal truncated by a crash may need the workspace session store rebuilt
  4. If two processes share one workspace, stop one — the journal is single-writer
Defensive patterns

Strategy: retry

Validate before calling

// Cheap pre-flight before paging events
if std::fs::metadata(workspace).is_err() {
    anyhow::bail!("workspace missing at {} — cannot read run journal", workspace.display());
}

Try / catch

// Retry transient sqlite busy locks, give up on real I/O errors
let mut attempt = 0;
loop {
    match read_run_events_page(workspace, run_id, offset, limit).await {
        Ok(page) => break Ok(page),
        Err(e) if attempt < 3 && e.to_string().contains("busy") => {
            attempt += 1;
            tokio::time::sleep(std::time::Duration::from_millis(200 * attempt)).await;
        }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: workspace path pointing at a moved/deleted directory; the journal sqlite file locked by a concurrently writing run (busy); corrupted journal db after a crash mid-write; read-only mount or permission change on the workspace.

Common situations: Inspecting a run's events while the run is still writing on a slow disk; restoring a workspace from backup; two core processes pointed at the same OPENHUMAN_WORKSPACE.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/c3ce767784e08f11. Report an issue: GitHub.