{"record":{"id":"c3ce767784e08f11","repo":"tinyhumansai/openhuman","slug":"agent-replay-read-run-events-page-failed-run-id","errorCode":null,"errorMessage":"[agent] replay read_run_events_page failed run_id={run_id}: {e}","messagePattern":"\\[agent\\] replay read_run_events_page failed run_id=(.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/agent/tinyagents/replay/ops.rs","lineNumber":67,"sourceCode":"pub(crate) async fn read_run_events_page(\n    workspace: &Path,\n    run_id: &str,\n    offset: u64,\n    limit: u64,\n) -> anyhow::Result<RunEventsPage> {\n    // Guard the page size: clamp a zero/absurd limit into [1, MAX].\n    let effective_limit = limit.clamp(1, MAX_EVENTS_LIMIT);\n    log::debug!(\n        \"[agent] replay read_run_events_page run_id={run_id} offset={offset} \\\n         limit={limit} effective_limit={effective_limit}\"\n    );\n\n    let stores = open_session_stores(workspace);\n    let journal = StoreEventJournal::new(stores.journal);\n    // Read one extra record to detect whether a further page exists without a\n    // second store round-trip.\n    let mut events = journal.read_from(run_id, offset).await.map_err(|e| {\n        anyhow::anyhow!(\"[agent] replay read_run_events_page failed run_id={run_id}: {e}\")\n    })?;\n\n    let has_more = events.len() as u64 > effective_limit;\n    if has_more {\n        events.truncate(effective_limit as usize);\n    }\n    // Offsets are monotonic within a run, so the cursor is simply \"one past the\n    // last returned offset\". `None` when this page drained the stream.\n    let next_offset = if has_more {\n        events.last().map(|obs| obs.offset + 1)\n    } else {\n        None\n    };\n\n    log::debug!(\n        \"[agent] replay read_run_events_page run_id={run_id} returned={} next_offset={:?}\",\n        events.len(),\n        next_offset","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/openhuman/agent/tinyagents/replay/ops.rs#L49-L85","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the workspace path exists and is writable by this process","Retry after the run that owns the journal finishes writing — sqlite busy locks are transient","Check disk space and filesystem health; a journal truncated by a crash may need the workspace session store rebuilt","If two processes share one workspace, stop one — the journal is single-writer"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Cheap pre-flight before paging events\nif std::fs::metadata(workspace).is_err() {\n    anyhow::bail!(\"workspace missing at {} — cannot read run journal\", workspace.display());\n}","typeGuard":null,"tryCatchPattern":"// Retry transient sqlite busy locks, give up on real I/O errors\nlet mut attempt = 0;\nloop {\n    match read_run_events_page(workspace, run_id, offset, limit).await {\n        Ok(page) => break Ok(page),\n        Err(e) if attempt < 3 && e.to_string().contains(\"busy\") => {\n            attempt += 1;\n            tokio::time::sleep(std::time::Duration::from_millis(200 * attempt)).await;\n        }\n        Err(e) => break Err(e),\n    }\n}","preventionTips":["Page a run's events from the process that owns the workspace whenever possible","Verify workspace existence and permissions before opening replay queries","Never point two writer processes at one workspace"],"tags":["rust","replay","sqlite","storage","io"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}