mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Materialize paginated thread history in SQLite (#32923)
## What changed - Project durable paginated rollout records into rebuildable SQLite tables for turns, items, and projection progress while keeping JSONL as the source of truth. - Add cursor-based local reads for turn summaries and thread or per-turn items, including archived-thread and history-mode validation. - Serialize writes, shutdown, and deletion per thread so projection updates cannot race cleanup, and remove projected rows when a thread is deleted. - Preserve stable item ordering while updating repeated snapshots and expose structured turn error details. ## Testing - Cover projection, snapshot updates, summary recovery, pagination, failure handling, shutdown flushing, legacy-mode rejection, and deletion races. GitOrigin-RevId: 34533185ece7f1f5690004c7f12b0f1f34dc2640
This commit is contained in:
1 parent
f96cf4d1c3
commit
0ef9fa4d65
15 files changed
+2104
-88
No files matched your search
@@ -0,0 +1,108 @@
|
||||
use std::io::SeekFrom;
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::DateTime;
|
||||
use codex_app_server_protocol::project_rollout_line;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tracing::warn;
|
||||
|
||||
use super::LocalThreadStore;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
|
||||
pub(super) async fn materialize_to_sqlite(
|
||||
store: &LocalThreadStore,
|
||||
thread_id: ThreadId,
|
||||
rollout_path: &Path,
|
||||
start_offset: u64,
|
||||
) -> ThreadStoreResult<()> {
|
||||
let (lines, next_offset) = read_durable_rollout_lines(rollout_path, start_offset).await?;
|
||||
if lines.is_empty() && start_offset == next_offset {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let projections = lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
let created_at_ms = DateTime::parse_from_rfc3339(line.timestamp.as_str())
|
||||
.map(|timestamp| timestamp.timestamp_millis())
|
||||
.map_err(thread_history_error)?;
|
||||
Ok((line.ordinal, created_at_ms, project_rollout_line(line)))
|
||||
})
|
||||
.collect::<ThreadStoreResult<Vec<_>>>()?;
|
||||
super::thread_history::apply_projection(
|
||||
store,
|
||||
thread_id,
|
||||
start_offset,
|
||||
next_offset,
|
||||
projections,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn read_durable_rollout_lines(
|
||||
rollout_path: &Path,
|
||||
start_offset: u64,
|
||||
) -> ThreadStoreResult<(Vec<RolloutLine>, u64)> {
|
||||
let next_offset = tokio::fs::metadata(rollout_path)
|
||||
.await
|
||||
.map_err(thread_store_io_error)?
|
||||
.len();
|
||||
let byte_count =
|
||||
next_offset
|
||||
.checked_sub(start_offset)
|
||||
.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: "durable rollout shrank before projection".to_string(),
|
||||
})?;
|
||||
let byte_count = usize::try_from(byte_count).map_err(|_| ThreadStoreError::Internal {
|
||||
message: "durable rollout append exceeds addressable memory".to_string(),
|
||||
})?;
|
||||
let mut bytes = vec![0; byte_count];
|
||||
let mut file = tokio::fs::File::open(rollout_path)
|
||||
.await
|
||||
.map_err(thread_store_io_error)?;
|
||||
file.seek(SeekFrom::Start(start_offset))
|
||||
.await
|
||||
.map_err(thread_store_io_error)?;
|
||||
file.read_exact(bytes.as_mut_slice())
|
||||
.await
|
||||
.map_err(thread_store_io_error)?;
|
||||
if bytes.last().is_some_and(|byte| *byte != b'\n') {
|
||||
return Err(ThreadStoreError::Internal {
|
||||
message: "durable rollout append is not newline terminated".to_string(),
|
||||
});
|
||||
}
|
||||
let text = std::str::from_utf8(bytes.as_slice()).map_err(thread_history_error)?;
|
||||
let mut lines = Vec::new();
|
||||
for line in text.lines().filter(|line| !line.is_empty()) {
|
||||
match serde_json::from_str(line) {
|
||||
Ok(line) => lines.push(line),
|
||||
Err(err) => {
|
||||
// A failed append can leave a partial record behind. The rollout writer repairs
|
||||
// its newline before retrying, so skip rejected lines just like the canonical
|
||||
// rollout loader and keep projecting the valid retry that follows.
|
||||
warn!("skipping rejected rollout line while projecting {rollout_path:?}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((lines, next_offset))
|
||||
}
|
||||
|
||||
fn thread_history_error(err: impl std::fmt::Display) -> ThreadStoreError {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!("failed to project thread history: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_io_error(err: std::io::Error) -> ThreadStoreError {
|
||||
ThreadStoreError::Internal {
|
||||
message: err.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "thread_history_materialization_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user