mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Unify thread metadata updates above store (#22236)
- make ThreadStore::update_thread_metadata accept a broad range of metadata patches - keep ThreadStore::append_items as raw canonical history append (no metadata side effects) - in the local store, write these metadata updates to a combination of sqlite and rollout jsonl files for backwards-compat. It special cases which fields need to go into jsonl vs sqlite vs whatever, confining the awkwardness to just this implementation - in remote stores we can simply persist the metadata directly to a database, no special casing required. - move the "implicit metadata updates triggered by appending rollout items" from the RolloutRecorder (which is local-threadstore-specific) to the LiveThread layer above the ThreadStore, inside of a private helper utility called ThreadMetadataSync. LiveThread calls ThreadStore append_items and update_metadata separately. - Add a generic update metadata method to ThreadManager that works on both live threads and "cold" threads - Call that ThreadManager method from app server code, so app server doesn't need to worry about whether the thread is live or not
This commit is contained in:
@@ -2846,12 +2846,21 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
token_usage: Option<&TokenUsage>,
|
||||
) {
|
||||
self.record_token_usage_info(turn_context, token_usage)
|
||||
.await;
|
||||
self.send_token_count_event(turn_context).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn record_token_usage_info(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
token_usage: Option<&TokenUsage>,
|
||||
) {
|
||||
if let Some(token_usage) = token_usage {
|
||||
let mut state = self.state.lock().await;
|
||||
state.update_token_info_from_usage(token_usage, turn_context.model_context_window());
|
||||
}
|
||||
self.send_token_count_event(turn_context).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn recompute_token_usage(&self, turn_context: &TurnContext) {
|
||||
@@ -2892,11 +2901,15 @@ impl Session {
|
||||
turn_context: &TurnContext,
|
||||
new_rate_limits: RateLimitSnapshot,
|
||||
) {
|
||||
self.record_rate_limits_info(new_rate_limits).await;
|
||||
self.send_token_count_event(turn_context).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn record_rate_limits_info(&self, new_rate_limits: RateLimitSnapshot) {
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_rate_limits(new_rate_limits);
|
||||
}
|
||||
self.send_token_count_event(turn_context).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn mcp_dependency_prompted(&self) -> HashSet<String> {
|
||||
@@ -2927,7 +2940,7 @@ impl Session {
|
||||
state.set_server_reasoning_included(included);
|
||||
}
|
||||
|
||||
async fn send_token_count_event(&self, turn_context: &TurnContext) {
|
||||
pub(crate) async fn send_token_count_event(&self, turn_context: &TurnContext) {
|
||||
let (info, rate_limits) = {
|
||||
let state = self.state.lock().await;
|
||||
state.token_info_and_rate_limits()
|
||||
|
||||
@@ -1872,6 +1872,7 @@ async fn try_run_sampling_request(
|
||||
Box<dyn ToolArgumentDiffConsumer>,
|
||||
)> = None;
|
||||
let mut should_emit_turn_diff = false;
|
||||
let mut should_emit_token_count = false;
|
||||
let reasoning_effort = turn_context.effective_reasoning_effort_for_tracing();
|
||||
let plan_mode = turn_context.collaboration_mode.mode == ModeKind::Plan;
|
||||
let mut assistant_message_stream_parsers = AssistantMessageStreamParsers::new(plan_mode);
|
||||
@@ -2098,7 +2099,8 @@ async fn try_run_sampling_request(
|
||||
ResponseEvent::RateLimits(snapshot) => {
|
||||
// Update internal state with latest rate limits, but defer sending until
|
||||
// token usage is available to avoid duplicate TokenCount events.
|
||||
sess.update_rate_limits(&turn_context, snapshot).await;
|
||||
sess.record_rate_limits_info(snapshot).await;
|
||||
should_emit_token_count = true;
|
||||
}
|
||||
ResponseEvent::ModelsEtag(etag) => {
|
||||
// Update internal state with latest models etag
|
||||
@@ -2116,8 +2118,9 @@ async fn try_run_sampling_request(
|
||||
&mut assistant_message_stream_parsers,
|
||||
)
|
||||
.await;
|
||||
sess.update_token_usage_info(&turn_context, token_usage.as_ref())
|
||||
sess.record_token_usage_info(&turn_context, token_usage.as_ref())
|
||||
.await;
|
||||
should_emit_token_count = true;
|
||||
should_emit_turn_diff = true;
|
||||
if let Some(false) = end_turn {
|
||||
needs_follow_up = true;
|
||||
@@ -2245,6 +2248,14 @@ async fn try_run_sampling_request(
|
||||
|
||||
drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?;
|
||||
|
||||
if should_emit_token_count {
|
||||
// A tool call such as request_user_input can intentionally pause the turn. Emit token
|
||||
// counts only after pending tools resolve so clients do not see progress events while the
|
||||
// turn is waiting on the user. This also needs to happen before returning cancellation so
|
||||
// token usage already recorded from the completed response is still persisted.
|
||||
sess.send_token_count_event(&turn_context).await;
|
||||
}
|
||||
|
||||
if cancellation_token.is_cancelled() {
|
||||
return Err(CodexErr::TurnAborted);
|
||||
}
|
||||
|
||||
@@ -58,8 +58,10 @@ use codex_thread_store::LocalThreadStoreConfig;
|
||||
use codex_thread_store::ReadThreadByRolloutPathParams;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use codex_thread_store::StoredThread;
|
||||
use codex_thread_store::ThreadMetadataPatch;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
use codex_thread_store::UpdateThreadMetadataParams;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
@@ -456,6 +458,44 @@ impl ThreadManager {
|
||||
self.state.get_thread(thread_id).await
|
||||
}
|
||||
|
||||
/// Updates metadata for loaded and cold threads through one entrypoint.
|
||||
///
|
||||
/// Loaded threads route through `CodexThread`/`LiveThread`, so metadata changes stay ordered
|
||||
/// with live rollout writes. Cold threads go directly to the store, which owns unloaded JSONL
|
||||
/// compatibility and SQLite metadata updates.
|
||||
pub async fn update_thread_metadata(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
patch: ThreadMetadataPatch,
|
||||
include_archived: bool,
|
||||
) -> CodexResult<StoredThread> {
|
||||
if let Ok(thread) = self.get_thread(thread_id).await {
|
||||
if thread.config_snapshot().await.ephemeral {
|
||||
return Err(CodexErr::InvalidRequest(format!(
|
||||
"ephemeral thread does not support metadata updates: {thread_id}"
|
||||
)));
|
||||
}
|
||||
return thread
|
||||
.update_thread_metadata(patch, include_archived)
|
||||
.await
|
||||
.map_err(|err| thread_store_metadata_update_error(thread_id, err));
|
||||
}
|
||||
self.state
|
||||
.thread_store
|
||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||
thread_id,
|
||||
patch,
|
||||
include_archived,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => {
|
||||
CodexErr::ThreadNotFound(thread_id)
|
||||
}
|
||||
err => thread_store_metadata_update_error(thread_id, err),
|
||||
})
|
||||
}
|
||||
|
||||
/// List `thread_id` plus all known descendants in its spawn subtree.
|
||||
pub async fn list_agent_subtree_thread_ids(
|
||||
&self,
|
||||
@@ -1298,6 +1338,19 @@ fn thread_store_rollout_read_error(err: ThreadStoreError) -> CodexErr {
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_metadata_update_error(thread_id: ThreadId, err: ThreadStoreError) -> CodexErr {
|
||||
match err {
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => CodexErr::ThreadNotFound(thread_id),
|
||||
ThreadStoreError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
|
||||
ThreadStoreError::Unsupported { operation } => CodexErr::UnsupportedOperation(format!(
|
||||
"thread metadata update is not supported by this store: {operation}"
|
||||
)),
|
||||
err => CodexErr::Fatal(format!(
|
||||
"failed to update thread metadata {thread_id}: {err}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a fork snapshot cut strictly before the nth user message (0-based).
|
||||
///
|
||||
/// Out-of-range values keep the full committed history at a turn boundary, but
|
||||
|
||||
Reference in New Issue
Block a user