Files
codex/codex-rs/thread-store/src/local/pending_thread_metadata.rs
T
Anton Panasenko fcdae21073 Support metadata staging for reserved thread IDs (#38819)
## Why

Callers need to associate host-owned state with a thread before Core starts it.

## What changed

- Add `ThreadManager::reserve_thread_id` and allow new threads to start with the reserved ID. Reject reserved IDs when resuming an existing thread.
- Let thread stores stage metadata for a reserved ID. The local store merges staged values into the first successful metadata update and then removes them.
- Clear staged metadata when an unmaterialized thread shuts down or is discarded, and require the state database for staging.

## Testing

- Cover reserved ID generation and resume validation.
- Cover metadata persistence, merging, rollout-compatible updates, cleanup, and invalid staging requests.

GitOrigin-RevId: 3f9bc48f0f7764011cc90ea01349e497dda0a9e3
2026-08-16 00:57:28 +00:00

59 lines
2.0 KiB
Rust

use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::sync::Arc;
use codex_protocol::ThreadId;
use tokio::sync::Mutex;
use tokio::sync::OwnedMutexGuard;
use crate::ThreadMetadataPatch;
use crate::ThreadStoreError;
use crate::ThreadStoreResult;
type PendingThreadMetadataEntry = Arc<Mutex<Option<ThreadMetadataPatch>>>;
/// In-memory metadata staged before a reserved thread starts.
///
/// The registry lock is held only while looking up or removing an entry. Never await an entry
/// lock while holding the registry lock. Callers may hold an entry lock before the live-writer
/// lock.
#[derive(Clone, Default)]
pub(super) struct PendingThreadMetadataRegistry {
entries: Arc<Mutex<HashMap<ThreadId, PendingThreadMetadataEntry>>>,
}
pub(super) type LockedPendingThreadMetadata = OwnedMutexGuard<Option<ThreadMetadataPatch>>;
impl PendingThreadMetadataRegistry {
pub(super) async fn stage(
&self,
thread_id: ThreadId,
patch: ThreadMetadataPatch,
) -> ThreadStoreResult<()> {
if patch.is_empty() {
return Err(ThreadStoreError::InvalidRequest {
message: "pending thread metadata cannot be empty".to_string(),
});
}
let mut entries = self.entries.lock().await;
match entries.entry(thread_id) {
Entry::Vacant(entry) => {
entry.insert(Arc::new(Mutex::new(Some(patch))));
Ok(())
}
Entry::Occupied(_) => Err(ThreadStoreError::InvalidRequest {
message: format!("pending thread metadata already exists: {thread_id}"),
}),
}
}
pub(super) async fn remove(&self, thread_id: ThreadId) {
self.entries.lock().await.remove(&thread_id);
}
pub(super) async fn lock(&self, thread_id: ThreadId) -> Option<LockedPendingThreadMetadata> {
let entry = self.entries.lock().await.get(&thread_id).cloned()?;
Some(entry.lock_owned().await)
}
}