mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
1 parent
a7913390f7
commit
5f79a92e39
67 files changed
+932
-162
No files matched your search
@@ -35,7 +35,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects hook context while classifying its actual receiving turn atomically.
|
||||
/// Injects hook context into the running turn atomically.
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn provenance and turn state updates must remain atomic"
|
||||
@@ -43,19 +43,13 @@ impl Session {
|
||||
pub(crate) async fn inject_hook_context_if_running(
|
||||
&self,
|
||||
input: Vec<ResponseItem>,
|
||||
source_turn_id: Option<&str>,
|
||||
) -> Result<(), Vec<ResponseItem>> {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
let Some(active_turn) = active.as_mut() else {
|
||||
return Err(input);
|
||||
};
|
||||
let Some(task) = active_turn.task.as_ref() else {
|
||||
if active_turn.task.is_none() {
|
||||
return Err(input);
|
||||
};
|
||||
if source_turn_id != Some(task.turn_context.sub_id.as_str()) {
|
||||
task.turn_context
|
||||
.turn_metadata_state
|
||||
.mark_root_turn_ambiguous();
|
||||
}
|
||||
self.input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
@@ -86,11 +80,6 @@ impl Session {
|
||||
.collect::<Vec<_>>();
|
||||
let mut active = self.active_turn.lock().await;
|
||||
if let Some(active_turn) = active.as_mut() {
|
||||
if let Some(task) = active_turn.task.as_ref() {
|
||||
task.turn_context
|
||||
.turn_metadata_state
|
||||
.mark_root_turn_ambiguous();
|
||||
}
|
||||
self.input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
|
||||
@@ -169,16 +169,16 @@ impl InputQueue {
|
||||
.and_then(|id| id.filter(|id| !id.trim().is_empty()).map(str::to_string));
|
||||
start_options.root_turn_id = pending_mails
|
||||
.iter()
|
||||
.filter(|mail| mail.communication.trigger_turn)
|
||||
.map(|mail| {
|
||||
.find(|mail| mail.communication.trigger_turn)
|
||||
.and_then(|mail| {
|
||||
mail.start_options
|
||||
.parent_turn_id
|
||||
.as_deref()
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
.and(mail.start_options.root_turn_id.as_deref())
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
})
|
||||
.reduce(|expected, candidate| expected.filter(|id| candidate == Some(*id)))
|
||||
.and_then(|id| id.filter(|id| !id.trim().is_empty()).map(str::to_string));
|
||||
.map(str::to_string);
|
||||
let items = pending_mails
|
||||
.into_iter()
|
||||
.map(|mail| TurnInput::InterAgentCommunication(mail.communication))
|
||||
@@ -320,17 +320,10 @@ impl InputQueue {
|
||||
}
|
||||
let (mailbox_items, start_options) = self.drain_mailbox_input_items().await;
|
||||
if let Some(active_turn_metadata) = active_turn_metadata
|
||||
&& mailbox_items.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
TurnInput::InterAgentCommunication(communication)
|
||||
if communication.trigger_turn
|
||||
)
|
||||
})
|
||||
&& (start_options.root_turn_id.is_none()
|
||||
|| active_turn_metadata.root_turn_id() != start_options.root_turn_id)
|
||||
&& active_turn_metadata.root_turn_id().is_none()
|
||||
&& let Some(root_turn_id) = start_options.root_turn_id.as_ref()
|
||||
{
|
||||
active_turn_metadata.mark_root_turn_ambiguous();
|
||||
active_turn_metadata.set_root_turn_id(root_turn_id.clone());
|
||||
}
|
||||
if pending_input.is_empty() {
|
||||
(mailbox_items, start_options)
|
||||
@@ -561,7 +554,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn input_queue_requires_one_unambiguous_trigger_parent() {
|
||||
async fn input_queue_uses_unambiguous_trigger_parent_and_first_root() {
|
||||
let (parent, peer, root, root2) = (Some("a"), Some("b"), Some("r"), Some("s"));
|
||||
for (pending_mails, expected_parent_turn_id, expected_root_turn_id) in [
|
||||
(Vec::new(), None, None),
|
||||
@@ -572,8 +565,8 @@ mod tests {
|
||||
(vec![(true, parent, None)], parent, None),
|
||||
(vec![(true, parent, Some(""))], parent, None),
|
||||
(vec![(true, parent, root), (true, peer, root)], None, root),
|
||||
(vec![(true, parent, root), (true, peer, root2)], None, None),
|
||||
(vec![(true, parent, root), (true, None, root)], None, None),
|
||||
(vec![(true, parent, root), (true, peer, root2)], None, root),
|
||||
(vec![(true, parent, root), (true, None, root)], None, root),
|
||||
(
|
||||
vec![(true, parent, root), (true, parent, root)],
|
||||
parent,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -294,6 +294,7 @@ impl Session {
|
||||
| RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::InterAgentCommunicationMetadata { .. } => {}
|
||||
}
|
||||
|
||||
@@ -385,6 +386,7 @@ impl Session {
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::SessionMeta(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +426,7 @@ impl Session {
|
||||
| RolloutItem::InterAgentCommunicationMetadata { .. }
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::EventMsg(_) => {
|
||||
unreachable!("only world-state replay items are collected")
|
||||
|
||||
@@ -265,6 +265,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -314,6 +315,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
let previous_model = "previous-rollout-model";
|
||||
let mut previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -1064,6 +1066,8 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
|
||||
codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 },
|
||||
@@ -1124,6 +1128,8 @@ async fn record_initial_history_requires_surviving_full_snapshot_without_user_tu
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -1156,6 +1162,8 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1226,6 +1234,8 @@ async fn reconstruct_history_prefers_compacted_window_over_session_meta() {
|
||||
first_window_id: Some(compacted_first_window_id.to_string()),
|
||||
previous_window_id: Some(compacted_previous_window_id.to_string()),
|
||||
window_id: Some(compacted_window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1262,6 +1272,8 @@ async fn reconstruct_history_replays_world_state_from_latest_compaction_window()
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::WorldState(WorldStateItem::full(object!({
|
||||
"environment": {"status": "starting", "cwd": "/workspace"}
|
||||
@@ -1324,6 +1336,8 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions() {
|
||||
first_window_id: Some(window_ids[0].to_string()),
|
||||
previous_window_id: Some(window_ids[window_number - 1].to_string()),
|
||||
window_id: Some(window_ids[window_number].to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::WorldState(WorldStateItem::full(object!({
|
||||
"environment": {"window": window_number, "status": "starting"}
|
||||
@@ -1420,6 +1434,8 @@ async fn reconstruct_history_preserves_legacy_compaction_count_with_session_meta
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1448,6 +1464,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1484,6 +1502,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
@@ -1532,6 +1552,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: Some("root-turn".to_string()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -1588,6 +1609,8 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
@@ -1624,6 +1647,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
.expect("serialize seeded reference context item"),
|
||||
serde_json::to_value(Some(TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: Some("root-turn".to_string()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -1658,6 +1682,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -1755,6 +1780,8 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1791,6 +1818,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
let unmatched_abort_turn_id = "other-turn".to_string();
|
||||
let current_context_item = TurnContextItem {
|
||||
turn_id: Some(current_turn_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -1920,6 +1948,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -2008,6 +2037,8 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2093,6 +2124,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -2182,6 +2214,8 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
// A newer TurnStarted replaces the incomplete compacted turn without a matching
|
||||
// completion/abort for the old one.
|
||||
|
||||
@@ -770,11 +770,12 @@ impl Session {
|
||||
}
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||||
};
|
||||
// Legacy subagent rollouts synthesize session_id from their own thread id.
|
||||
// Legacy subagent rollouts synthesized session_id from their own thread ID.
|
||||
let resumed_session_id = resumed_session_id.filter(|session_id| {
|
||||
!session_configuration.session_source.is_non_root_agent()
|
||||
|| *session_id != SessionId::from(thread_id)
|
||||
});
|
||||
// session_id is equal to the root thread's ID.
|
||||
let session_id = resumed_session_id.unwrap_or_else(|| {
|
||||
if session_configuration.session_source.is_non_root_agent() {
|
||||
agent_control.session_id()
|
||||
@@ -1357,6 +1358,7 @@ impl Session {
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::SecurityRiskScore(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TokenUsageRecord;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
@@ -2227,6 +2228,8 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
|
||||
first_window_id: Some(first_window_id.to_string()),
|
||||
previous_window_id: Some(previous_window_id.to_string()),
|
||||
window_id: Some(window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
})];
|
||||
|
||||
let reconstructed = session
|
||||
@@ -2883,6 +2886,49 @@ async fn record_initial_history_seeds_token_info_from_rollout() {
|
||||
assert_eq!(actual, Some(info2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_token_usage_record_stops_at_compaction_checkpoint() {
|
||||
let thread_id = ThreadId::new();
|
||||
let checkpoint_record = TokenUsageRecord {
|
||||
thread_id,
|
||||
turn_id: "turn-1".to_string(),
|
||||
session_id: SessionId::from(thread_id),
|
||||
root_turn_id: "turn-1".to_string(),
|
||||
response_id: "response-1".to_string(),
|
||||
usage: TokenUsage::default(),
|
||||
turn_token_usage: TokenUsage::default(),
|
||||
thread_token_usage: TokenUsage::default(),
|
||||
};
|
||||
let checkpoint = |latest_token_usage_record| {
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record,
|
||||
})
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Session::last_token_usage_record_from_rollout(&[
|
||||
RolloutItem::TokenUsageRecord(checkpoint_record.clone()),
|
||||
checkpoint(Some(checkpoint_record.clone())),
|
||||
]),
|
||||
Some(checkpoint_record.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
Session::last_token_usage_record_from_rollout(&[
|
||||
RolloutItem::TokenUsageRecord(checkpoint_record),
|
||||
checkpoint(None),
|
||||
]),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recompute_token_usage_uses_session_base_instructions() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
@@ -3412,7 +3458,7 @@ async fn record_initial_history_reconstructs_forked_transcript() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_new_context_window_assigns_and_persists_item_ids() {
|
||||
async fn start_new_context_window_persists_checkpoint_state() {
|
||||
let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
@@ -3421,6 +3467,18 @@ async fn start_new_context_window_assigns_and_persists_item_ids() {
|
||||
.await;
|
||||
let rollout_path =
|
||||
attach_thread_persistence(Arc::get_mut(&mut session).expect("unique session")).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let token_usage_record = TokenUsageRecord {
|
||||
thread_id,
|
||||
turn_id: "turn-1".to_string(),
|
||||
session_id: SessionId::from(thread_id),
|
||||
root_turn_id: "turn-1".to_string(),
|
||||
response_id: "response-1".to_string(),
|
||||
usage: TokenUsage::default(),
|
||||
turn_token_usage: TokenUsage::default(),
|
||||
thread_token_usage: TokenUsage::default(),
|
||||
};
|
||||
session.state.lock().await.latest_token_usage_record = Some(token_usage_record.clone());
|
||||
let step_context = session
|
||||
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
|
||||
.await
|
||||
@@ -3447,8 +3505,8 @@ async fn start_new_context_window_assigns_and_persists_item_ids() {
|
||||
else {
|
||||
panic!("expected resumed rollout history");
|
||||
};
|
||||
let persisted_replacement_history = resumed.history.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::Compacted(compacted) => compacted.replacement_history.as_ref(),
|
||||
let persisted_compacted = resumed.history.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::Compacted(compacted) => Some(compacted),
|
||||
RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_)
|
||||
| RolloutItem::InterAgentCommunication(_)
|
||||
@@ -3456,13 +3514,23 @@ async fn start_new_context_window_assigns_and_persists_item_ids() {
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
});
|
||||
assert_eq!(
|
||||
persisted_replacement_history.cloned(),
|
||||
persisted_compacted.and_then(|compacted| compacted.replacement_history.clone()),
|
||||
Some(live_history.annotated_items().to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
persisted_compacted.map(|compacted| {
|
||||
(
|
||||
compacted.compaction_response_id.as_deref(),
|
||||
compacted.latest_token_usage_record.as_ref(),
|
||||
)
|
||||
}),
|
||||
Some((None, Some(&token_usage_record)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3535,6 +3603,7 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
});
|
||||
@@ -3691,6 +3760,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
let previous_model = "forked-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
root_turn_id: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
workspace_roots: None,
|
||||
@@ -4102,6 +4172,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
||||
first_window_id: Some(first_window_id.to_string()),
|
||||
previous_window_id: Some(previous_window_id.to_string()),
|
||||
window_id: Some(compacted_window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: compact_turn_id,
|
||||
@@ -5864,6 +5936,7 @@ async fn compaction_checkpoint_waits_for_accepted_settings_persistence() {
|
||||
message: "summary".to_string(),
|
||||
window_number,
|
||||
window_ids,
|
||||
compaction_response_id: None,
|
||||
},
|
||||
),
|
||||
));
|
||||
@@ -11907,6 +11980,66 @@ async fn trigger_turn_mailbox_mail_waits_for_next_turn_after_answer_boundary() {
|
||||
assert!(sess.input_queue.has_trigger_turn_mailbox_items().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn active_turn_keeps_first_root_when_mail_coalesces() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
tc.turn_metadata_state
|
||||
.set_root_turn_id("root-a".to_string());
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let first = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker_a").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"first".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
let second = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker_b").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"second".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
for (communication, parent_turn_id, root_turn_id) in [
|
||||
(first.clone(), "parent-a", "root-a"),
|
||||
(second.clone(), "parent-b", "root-b"),
|
||||
] {
|
||||
sess.input_queue
|
||||
.enqueue_mailbox_communication(
|
||||
communication,
|
||||
codex_protocol::turn_input::TurnStartOptions {
|
||||
parent_turn_id: Some(parent_turn_id.to_string()),
|
||||
root_turn_id: Some(root_turn_id.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
(sess.input_queue.get_pending_input(&sess.active_turn).await).0,
|
||||
vec![
|
||||
TurnInput::InterAgentCommunication(first),
|
||||
TurnInput::InterAgentCommunication(second),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
tc.turn_metadata_state.root_turn_id().as_deref(),
|
||||
Some("root-a")
|
||||
);
|
||||
assert!(!sess.input_queue.has_pending_mailbox_items().await);
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
@@ -12271,6 +12404,8 @@ async fn sample_rollout(
|
||||
first_window_id: Some(window_ids.first_window_id.to_string()),
|
||||
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
|
||||
window_id: Some(window_ids.window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}));
|
||||
|
||||
let user2 = user_message("second user");
|
||||
@@ -12301,6 +12436,8 @@ async fn sample_rollout(
|
||||
first_window_id: Some(window_ids.first_window_id.to_string()),
|
||||
previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()),
|
||||
window_id: Some(window_ids.window_id.to_string()),
|
||||
compaction_response_id: None,
|
||||
latest_token_usage_record: None,
|
||||
}));
|
||||
|
||||
let user3 = user_message("third user");
|
||||
|
||||
@@ -100,7 +100,6 @@ use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InternalSessionSource;
|
||||
use codex_protocol::protocol::PlanDeltaEvent;
|
||||
use codex_protocol::protocol::RawResponseCompletedEvent;
|
||||
use codex_protocol::protocol::ReasoningContentDeltaEvent;
|
||||
use codex_protocol::protocol::ReasoningRawContentDeltaEvent;
|
||||
use codex_protocol::protocol::SafetyBufferingEvent;
|
||||
@@ -2590,13 +2589,11 @@ async fn try_run_sampling_request(
|
||||
&mut assistant_message_stream_parsers,
|
||||
)
|
||||
.await;
|
||||
sess.send_event(
|
||||
sess.record_observed_response_completed(
|
||||
&turn_context,
|
||||
EventMsg::RawResponseCompleted(RawResponseCompletedEvent {
|
||||
response_id,
|
||||
token_usage: token_usage.clone(),
|
||||
usage_metadata,
|
||||
}),
|
||||
&response_id,
|
||||
token_usage.as_ref(),
|
||||
usage_metadata.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let budget_result = sess
|
||||
|
||||
@@ -578,6 +578,7 @@ impl TurnContext {
|
||||
let cwd = self.cwd.clone();
|
||||
TurnContextItem {
|
||||
turn_id: Some(self.sub_id.clone()),
|
||||
root_turn_id: self.turn_metadata_state.root_turn_id(),
|
||||
cwd,
|
||||
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
|
||||
current_date: self.current_date.clone(),
|
||||
|
||||
@@ -624,13 +624,17 @@ impl Session {
|
||||
input => pending_turn_input(input.clone()),
|
||||
};
|
||||
pending_input.push(input);
|
||||
if let Some(incoming_root_turn_id) = incoming_root_turn_id
|
||||
&& active_task.turn_context.turn_metadata_state.root_turn_id() != incoming_root_turn_id
|
||||
if active_task
|
||||
.turn_context
|
||||
.turn_metadata_state
|
||||
.root_turn_id()
|
||||
.is_none()
|
||||
&& let Some(Some(incoming_root_turn_id)) = incoming_root_turn_id
|
||||
{
|
||||
active_task
|
||||
.turn_context
|
||||
.turn_metadata_state
|
||||
.mark_root_turn_ambiguous();
|
||||
.set_root_turn_id(incoming_root_turn_id);
|
||||
}
|
||||
self.input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
|
||||
Reference in new issue
Block a user