Persist response token usage in rollout history (#41912)

## Why

Resumed threads need to continue per-turn and per-thread token totals without scanning arbitrarily far beyond the latest compaction checkpoint.

## What changed

- Add durable `TokenUsageRecord` rollout items with response, turn, thread, session, and root-turn attribution.
- Restore accumulated usage on resume and snapshot the latest record plus the compaction response ID in compaction checkpoints.
- Preserve root-turn lineage in persisted turn context, while ensuring forked child threads start with their own usage totals.

## Testing

- Cover usage accumulation across multiple responses and resumed turns.
- Cover local and remote compaction checkpoints, invalid remote compaction output, rollout reconstruction, and fork isolation.

GitOrigin-RevId: ef9e0c4a9102a08a2c382be4cdac68c84353c90a
This commit is contained in:
Owen Lin authored and copyberry committed 2026-08-31 21:15:38 +00:00
1 parent a7913390f7
commit 5f79a92e39
67 files changed
+932 -162

No files matched your search

+59
View File
@@ -90,6 +90,7 @@ use codex_network_proxy::normalize_host;
use codex_otel::current_span_trace_id;
use codex_otel::current_span_w3c_trace_context;
use codex_otel::set_parent_from_w3c_trace_context;
use codex_protocol::ResponseUsageMetadata;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ElicitationRequest;
@@ -349,6 +350,7 @@ use codex_protocol::protocol::ModelVerificationEvent;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RawResponseCompletedEvent;
use codex_protocol::protocol::RequestUserInputEvent;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
@@ -360,6 +362,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TokenUsageRecord;
use codex_protocol::protocol::TurnModerationMetadataEvent;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::turn_input::TurnInputMode;
@@ -1440,6 +1443,8 @@ impl Session {
let mut state = self.state.lock().await;
state.set_token_info(Some(info));
}
self.state.lock().await.latest_token_usage_record =
Self::last_token_usage_record_from_rollout(&rollout_items);
// Defer seeding the session's initial context until the first turn starts so
// turn/start overrides can be merged before we write to the rollout.
@@ -1460,6 +1465,8 @@ impl Session {
let mut state = self.state.lock().await;
state.set_token_info(Some(info));
}
self.state.lock().await.latest_token_usage_record =
Self::last_token_usage_record_from_rollout(&rollout_items);
let thread_settings_applied =
RolloutItem::EventMsg(thread_settings::applied_event(self).await);
@@ -1624,6 +1631,21 @@ impl Session {
})
}
fn last_token_usage_record_from_rollout(
rollout_items: &[RolloutItem],
) -> Option<TokenUsageRecord> {
for item in rollout_items.iter().rev() {
match item {
RolloutItem::TokenUsageRecord(record) => return Some(record.clone()),
RolloutItem::Compacted(compacted) => {
return compacted.latest_token_usage_record.clone();
}
_ => {}
}
}
None
}
async fn previous_turn_settings(&self) -> Option<PreviousTurnSettings> {
let state = self.state.lock().await;
state.previous_turn_settings()
@@ -3668,6 +3690,8 @@ impl Session {
.previous_window_id
.map(|id| id.to_string()),
window_id: Some(metadata.window_ids.window_id.to_string()),
compaction_response_id: metadata.compaction_response_id,
latest_token_usage_record: self.state.lock().await.latest_token_usage_record.clone(),
};
// Wait for accepted updates to finish persisting, then keep later updates from
// overtaking the current settings snapshot while its checkpoint is written.
@@ -4118,6 +4142,7 @@ impl Session {
message: String::new(),
window_number,
window_ids,
compaction_response_id: None,
},
)
.await;
@@ -4232,6 +4257,40 @@ impl Session {
result
}
pub(crate) async fn record_observed_response_completed(
&self,
turn_context: &TurnContext,
response_id: &str,
usage: Option<&TokenUsage>,
usage_metadata: Option<&ResponseUsageMetadata>,
) {
self.send_event(
turn_context,
EventMsg::RawResponseCompleted(RawResponseCompletedEvent {
response_id: response_id.to_string(),
token_usage: usage.cloned(),
usage_metadata: usage_metadata.cloned(),
}),
)
.await;
let Some(usage) = usage else {
return;
};
let record = self.state.lock().await.record_token_usage(
self.thread_id,
&turn_context.sub_id,
self.session_id(),
turn_context
.turn_metadata_state
.root_turn_id()
.unwrap_or_else(|| turn_context.sub_id.clone()),
response_id.to_string(),
usage,
);
self.persist_rollout_items(&[RolloutItem::TokenUsageRecord(record)])
.await;
}
pub(crate) async fn record_token_usage_info(
&self,
turn_context: &TurnContext,