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
@@ -7,6 +7,8 @@ mod live_writer;
|
||||
mod model_context;
|
||||
mod read_thread;
|
||||
mod search_threads;
|
||||
mod thread_history;
|
||||
mod thread_history_materialization;
|
||||
mod unarchive_thread;
|
||||
mod update_thread_metadata;
|
||||
|
||||
@@ -22,12 +24,17 @@ use std::collections::hash_map::Entry;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::sync::OwnedMutexGuard;
|
||||
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
@@ -42,6 +49,7 @@ use crate::ThreadStore;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreFuture;
|
||||
use crate::ThreadStoreResult;
|
||||
use crate::TurnPage;
|
||||
use crate::UpdateThreadMetadataParams;
|
||||
|
||||
/// Local filesystem/SQLite-backed implementation of [`ThreadStore`].
|
||||
@@ -61,7 +69,9 @@ use crate::UpdateThreadMetadataParams;
|
||||
pub struct LocalThreadStore {
|
||||
pub(super) config: LocalThreadStoreConfig,
|
||||
live_recorders: Arc<Mutex<HashMap<ThreadId, LiveRecorderEntry>>>,
|
||||
live_writer_locks: Arc<LiveWriterLocks>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_history_db: Arc<OnceCell<sqlx::SqlitePool>>,
|
||||
}
|
||||
|
||||
struct LiveRecorderEntry {
|
||||
@@ -72,6 +82,26 @@ struct LiveRecorderEntry {
|
||||
history_mode: ThreadHistoryMode,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LiveWriterLocks {
|
||||
// Keep per-thread locks after a writer goes idle. Removing one while another caller is about
|
||||
// to acquire it could let two operations for the same thread run at once.
|
||||
by_thread: Mutex<HashMap<ThreadId, Arc<Mutex<()>>>>,
|
||||
}
|
||||
|
||||
impl LiveWriterLocks {
|
||||
async fn lock(&self, thread_id: ThreadId) -> OwnedMutexGuard<()> {
|
||||
let lock = self
|
||||
.by_thread
|
||||
.lock()
|
||||
.await
|
||||
.entry(thread_id)
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.clone();
|
||||
lock.lock_owned().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Process-scoped configuration for local thread storage.
|
||||
///
|
||||
/// This describes where local storage lives. New-thread rollout metadata such
|
||||
@@ -108,7 +138,9 @@ impl LocalThreadStore {
|
||||
Self {
|
||||
config,
|
||||
live_recorders: Arc::new(Mutex::new(HashMap::new())),
|
||||
live_writer_locks: Arc::new(LiveWriterLocks::default()),
|
||||
state_db,
|
||||
thread_history_db: Arc::new(OnceCell::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +149,17 @@ impl LocalThreadStore {
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
async fn thread_history_db(&self) -> ThreadStoreResult<&sqlx::SqlitePool> {
|
||||
self.thread_history_db
|
||||
.get_or_try_init(|| async {
|
||||
codex_state::open_thread_history_db(self.config.sqlite_home.as_path()).await
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to open thread history database: {err}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a local rollout-backed thread by path.
|
||||
pub async fn read_thread_by_rollout_path(
|
||||
&self,
|
||||
@@ -138,18 +181,6 @@ impl LocalThreadStore {
|
||||
live_writer::rollout_path(self, thread_id).await
|
||||
}
|
||||
|
||||
pub(super) async fn live_recorder(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> ThreadStoreResult<RolloutRecorder> {
|
||||
self.live_recorders
|
||||
.lock()
|
||||
.await
|
||||
.get(&thread_id)
|
||||
.map(|entry| entry.recorder.clone())
|
||||
.ok_or(ThreadStoreError::ThreadNotFound { thread_id })
|
||||
}
|
||||
|
||||
pub(super) async fn ensure_live_recorder_absent(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
@@ -237,6 +268,16 @@ impl LocalThreadStore {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Lists projection-backed turns without enabling app-server routing yet.
|
||||
pub async fn list_turns(&self, params: ListTurnsParams) -> ThreadStoreResult<TurnPage> {
|
||||
thread_history::list_turns(self, params).await
|
||||
}
|
||||
|
||||
/// Lists projection-backed items without enabling app-server routing yet.
|
||||
pub async fn list_items(&self, params: ListItemsParams) -> ThreadStoreResult<ItemPage> {
|
||||
thread_history::list_items(self, params).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadStore for LocalThreadStore {
|
||||
|
||||
Reference in new issue
Block a user