mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
## What changed - Add the disabled-by-default `output_token_replay` feature to request `output.encrypted_content` from OpenAI providers. - Preserve encrypted message and tool-call output, message and function-call status, and output-text `annotations` and `logprobs` through history and replay. Update the generated protocol schemas and SDK types. - Strip output ciphertext from requests to other providers while retaining it in stored history, including after replay is disabled. Remove encrypted content from memory extraction inputs. ## Testing Add integration coverage for the feature/provider combinations, metadata replay across follow-up requests, and history preservation when resuming with replay disabled. GitOrigin-RevId: 77729a021b6771933402aebe74ece8eb39c057e2
872 lines
34 KiB
Rust
872 lines
34 KiB
Rust
use crate::model::ThreadMetadata;
|
|
use codex_history::RolloutItem;
|
|
use codex_protocol::items::TurnItem;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::SessionMetaLine;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_protocol::protocol::TurnContextItem;
|
|
use codex_protocol::protocol::UserMessageEvent;
|
|
use codex_protocol::protocol::strip_user_message_prefix;
|
|
use codex_protocol::protocol::user_message_preview;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
pub const GUARDIAN_THREAD_TITLE: &str = "Guardian review";
|
|
pub const GUARDIAN_THREAD_PREVIEW: &str = "Approval review";
|
|
|
|
/// Identifies internal review threads whose user messages are synthetic approval prompts.
|
|
pub fn is_guardian_review_source(source: &SessionSource) -> bool {
|
|
matches!(
|
|
source,
|
|
SessionSource::SubAgent(SubAgentSource::Other(name)) if name == "guardian"
|
|
)
|
|
}
|
|
|
|
/// Apply a rollout item to the metadata structure.
|
|
pub fn apply_rollout_item(
|
|
metadata: &mut ThreadMetadata,
|
|
item: &RolloutItem,
|
|
default_provider: &str,
|
|
) {
|
|
match item {
|
|
RolloutItem::SessionMeta(meta_line) => apply_session_meta_from_item(metadata, meta_line),
|
|
RolloutItem::TurnContext(turn_ctx) => apply_turn_context(metadata, turn_ctx),
|
|
RolloutItem::EventMsg(event) => apply_event_msg(metadata, event),
|
|
RolloutItem::ResponseItem(item) => apply_response_item(metadata, &item.item),
|
|
RolloutItem::InterAgentCommunication(_)
|
|
| RolloutItem::InterAgentCommunicationMetadata { .. } => {}
|
|
RolloutItem::Compacted(_) => {}
|
|
RolloutItem::WorldState(_) => {}
|
|
RolloutItem::RetainedContext(_) | RolloutItem::SecurityRiskScore(_) => {}
|
|
RolloutItem::RealtimeItem(_) => {}
|
|
RolloutItem::TokenUsageRecord(_) => {}
|
|
}
|
|
if metadata.model_provider.is_empty() {
|
|
metadata.model_provider = default_provider.to_string();
|
|
}
|
|
}
|
|
|
|
/// Return whether this rollout item can mutate thread metadata stored in SQLite.
|
|
pub fn rollout_item_affects_thread_metadata(item: &RolloutItem) -> bool {
|
|
match item {
|
|
RolloutItem::SessionMeta(_) | RolloutItem::TurnContext(_) => true,
|
|
RolloutItem::EventMsg(
|
|
EventMsg::TokenCount(_)
|
|
| EventMsg::UserMessage(_)
|
|
| EventMsg::ThreadGoalUpdated(_)
|
|
| EventMsg::ThreadSettingsApplied(_),
|
|
) => true,
|
|
RolloutItem::EventMsg(EventMsg::ItemCompleted(event))
|
|
if matches!(
|
|
event.item,
|
|
TurnItem::UserMessage(_) | TurnItem::FunctionCallOutput(_)
|
|
) =>
|
|
{
|
|
true
|
|
}
|
|
RolloutItem::EventMsg(_)
|
|
| RolloutItem::ResponseItem(_)
|
|
| RolloutItem::InterAgentCommunication(_)
|
|
| RolloutItem::InterAgentCommunicationMetadata { .. }
|
|
| RolloutItem::Compacted(_)
|
|
| RolloutItem::RealtimeItem(_)
|
|
| RolloutItem::RetainedContext(_)
|
|
| RolloutItem::SecurityRiskScore(_)
|
|
| RolloutItem::TokenUsageRecord(_)
|
|
| RolloutItem::WorldState(_) => false,
|
|
}
|
|
}
|
|
|
|
fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &SessionMetaLine) {
|
|
if metadata.id != meta_line.meta.id {
|
|
// Ignore session_meta lines that don't match the canonical thread ID,
|
|
// e.g., forked rollouts that embed the source session metadata.
|
|
return;
|
|
}
|
|
metadata.creator_user_id = metadata
|
|
.creator_user_id
|
|
.take()
|
|
.or_else(|| meta_line.meta.creator_user_id.clone());
|
|
metadata.creator_account_id = metadata
|
|
.creator_account_id
|
|
.take()
|
|
.or_else(|| meta_line.meta.creator_account_id.clone());
|
|
metadata.id = meta_line.meta.id;
|
|
metadata.source = enum_to_string(&meta_line.meta.source);
|
|
if metadata.originator.is_none() && !meta_line.meta.originator.is_empty() {
|
|
metadata.originator = Some(meta_line.meta.originator.clone());
|
|
}
|
|
// Later SessionMeta lines do not redefine the canonical history_mode.
|
|
metadata.thread_source = meta_line.meta.thread_source.clone();
|
|
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
|
metadata.agent_role = meta_line.meta.agent_role.clone();
|
|
metadata.agent_path = meta_line.meta.agent_path.clone();
|
|
if let Some(provider) = meta_line.meta.model_provider.as_deref() {
|
|
metadata.model_provider = provider.to_string();
|
|
}
|
|
if !meta_line.meta.cli_version.is_empty() {
|
|
metadata.cli_version = meta_line.meta.cli_version.clone();
|
|
}
|
|
if !meta_line.meta.cwd.as_os_str().is_empty() {
|
|
metadata.cwd = meta_line.meta.cwd.clone();
|
|
}
|
|
if let Some(git) = meta_line.git.as_ref() {
|
|
metadata.git_sha = git.commit_hash.as_ref().map(|sha| sha.0.clone());
|
|
metadata.git_branch = git.branch.clone();
|
|
metadata.git_origin_url = git.repository_url.clone();
|
|
}
|
|
}
|
|
|
|
fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) {
|
|
if metadata.cwd.as_os_str().is_empty() {
|
|
metadata.cwd = turn_ctx.cwd.clone().into_path_buf();
|
|
}
|
|
metadata.model = Some(turn_ctx.model.clone());
|
|
metadata.reasoning_effort = turn_ctx.effort.clone();
|
|
metadata.sandbox_policy =
|
|
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
|
|
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
|
|
}
|
|
|
|
fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
|
|
match event {
|
|
EventMsg::TokenCount(token_count) => {
|
|
if let Some(info) = token_count.info.as_ref() {
|
|
metadata.tokens_used = info.total_token_usage.total_tokens.max(0);
|
|
}
|
|
}
|
|
EventMsg::UserMessage(user) if !metadata_is_guardian_review(metadata) => {
|
|
apply_user_message(metadata, user);
|
|
}
|
|
EventMsg::ItemCompleted(event) => {
|
|
if let TurnItem::UserMessage(user) = &event.item
|
|
&& !metadata_is_guardian_review(metadata)
|
|
{
|
|
apply_user_message(metadata, &user.as_legacy_user_message_event());
|
|
} else if let TurnItem::FunctionCallOutput(output) = &event.item
|
|
&& !metadata_is_guardian_review(metadata)
|
|
{
|
|
// Delegated turns have no user message, but must still be discoverable.
|
|
let preview = crate::delegated_output_preview(output);
|
|
set_preview_if_empty(metadata, preview);
|
|
}
|
|
}
|
|
EventMsg::ThreadGoalUpdated(event) if !metadata_is_guardian_review(metadata) => {
|
|
let objective = event.goal.objective.trim();
|
|
if !objective.is_empty() {
|
|
set_preview_if_empty(metadata, Some(objective.to_string()));
|
|
}
|
|
}
|
|
EventMsg::ThreadSettingsApplied(event) => {
|
|
let settings = &event.thread_settings;
|
|
metadata.model = Some(settings.model.clone());
|
|
metadata.model_provider = settings.model_provider_id.clone();
|
|
metadata.reasoning_effort = settings.reasoning_effort.clone();
|
|
metadata.cwd = settings.cwd.clone().into_path_buf();
|
|
metadata.sandbox_policy =
|
|
serde_json::to_string(&settings.permission_profile).unwrap_or_default();
|
|
metadata.approval_mode = enum_to_string(&settings.approval_policy);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn apply_response_item(_metadata: &mut ThreadMetadata, _item: &ResponseItem) {}
|
|
|
|
pub(crate) fn metadata_is_guardian_review(metadata: &ThreadMetadata) -> bool {
|
|
serde_json::from_str::<SessionSource>(metadata.source.as_str())
|
|
.as_ref()
|
|
.is_ok_and(is_guardian_review_source)
|
|
}
|
|
|
|
fn apply_user_message(metadata: &mut ThreadMetadata, user: &UserMessageEvent) {
|
|
let preview = user_message_preview(user);
|
|
if metadata.first_user_message.is_none() {
|
|
metadata.first_user_message = preview.clone();
|
|
}
|
|
set_preview_if_empty(metadata, preview);
|
|
if metadata.title.is_empty() {
|
|
let title = strip_user_message_prefix(user.message.as_str());
|
|
if !title.is_empty() {
|
|
metadata.title = title.to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_preview_if_empty(metadata: &mut ThreadMetadata, preview: Option<String>) {
|
|
if metadata.preview.is_none() {
|
|
metadata.preview = preview;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn enum_to_string<T: Serialize>(value: &T) -> String {
|
|
match serde_json::to_value(value) {
|
|
Ok(Value::String(s)) => s,
|
|
Ok(other) => other.to_string(),
|
|
Err(_) => String::new(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::apply_rollout_item;
|
|
use super::rollout_item_affects_thread_metadata;
|
|
use crate::ThreadMetadataBuilder;
|
|
use crate::model::ThreadMetadata;
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use codex_history::RolloutItem;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ApprovalsReviewer;
|
|
use codex_protocol::config_types::CollaborationMode;
|
|
use codex_protocol::config_types::ModeKind;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::config_types::Settings;
|
|
use codex_protocol::items::TurnItem;
|
|
use codex_protocol::items::UserMessageItem;
|
|
use codex_protocol::models::ContentItem;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::ItemCompletedEvent;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionMeta;
|
|
use codex_protocol::protocol::SessionMetaLine;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_protocol::protocol::ThreadGoal;
|
|
use codex_protocol::protocol::ThreadGoalStatus;
|
|
use codex_protocol::protocol::ThreadGoalUpdatedEvent;
|
|
use codex_protocol::protocol::ThreadHistoryMode;
|
|
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
|
|
use codex_protocol::protocol::ThreadSettingsSnapshot;
|
|
use codex_protocol::protocol::TurnContextItem;
|
|
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
|
|
use codex_protocol::protocol::UserMessageEvent;
|
|
use codex_protocol::user_input::UserInput;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
use uuid::Uuid;
|
|
|
|
#[test]
|
|
fn response_item_user_messages_do_not_set_title_or_first_user_message() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::ResponseItem(
|
|
ResponseItem::Message {
|
|
status: None,
|
|
encrypted_content: None,
|
|
id: None,
|
|
role: "user".to_string(),
|
|
content: vec![ContentItem::InputText {
|
|
text: "hello from response item".to_string(),
|
|
}],
|
|
phase: None,
|
|
internal_chat_message_metadata_passthrough: None,
|
|
}
|
|
.into(),
|
|
);
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(metadata.first_user_message, None);
|
|
assert_eq!(metadata.preview, None);
|
|
assert_eq!(metadata.title, "");
|
|
}
|
|
|
|
#[test]
|
|
fn event_msg_user_messages_set_title_and_first_user_message() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
client_id: None,
|
|
message: format!("{USER_MESSAGE_BEGIN} actual user request"),
|
|
images: Some(vec![]),
|
|
local_images: vec![],
|
|
text_elements: vec![],
|
|
..Default::default()
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(
|
|
metadata.first_user_message.as_deref(),
|
|
Some("actual user request")
|
|
);
|
|
assert_eq!(metadata.preview.as_deref(), Some("actual user request"));
|
|
assert_eq!(metadata.title, "actual user request");
|
|
}
|
|
|
|
#[test]
|
|
fn delegated_turn_preview_preserves_user_message_metadata() {
|
|
let mut metadata = metadata_for_test();
|
|
let mut expected = metadata.clone();
|
|
for text in [" ", "delegated task", "later output"] {
|
|
let item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
|
|
thread_id: metadata.id,
|
|
turn_id: "turn-1".to_string(),
|
|
item: TurnItem::FunctionCallOutput(codex_protocol::items::FunctionCallOutputItem {
|
|
id: "output-1".to_string(),
|
|
name: "create_thread".to_string(),
|
|
namespace: Some("codex_app".to_string()),
|
|
output: codex_protocol::models::FunctionCallOutputBody::Text(text.to_string()),
|
|
}),
|
|
started_at_ms: Some(0),
|
|
completed_at_ms: 0,
|
|
}));
|
|
assert!(rollout_item_affects_thread_metadata(&item));
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
if text == "delegated task" {
|
|
expected.preview = Some(text.to_string());
|
|
}
|
|
assert_eq!(metadata, expected);
|
|
}
|
|
let user_item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
message: "actual user follow-up".to_string(),
|
|
..Default::default()
|
|
}));
|
|
apply_rollout_item(&mut metadata, &user_item, "test-provider");
|
|
expected.first_user_message = Some("actual user follow-up".to_string());
|
|
expected.title = "actual user follow-up".to_string();
|
|
assert_eq!(metadata, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn completed_user_message_items_set_title_and_first_user_message() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent {
|
|
thread_id: ThreadId::default(),
|
|
turn_id: "turn-1".to_string(),
|
|
item: TurnItem::UserMessage(UserMessageItem::new(&[UserInput::Text {
|
|
text: format!("{USER_MESSAGE_BEGIN} actual user request"),
|
|
text_elements: Vec::new(),
|
|
}])),
|
|
started_at_ms: Some(0),
|
|
completed_at_ms: 0,
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(
|
|
metadata.first_user_message.as_deref(),
|
|
Some("actual user request")
|
|
);
|
|
assert_eq!(metadata.preview.as_deref(), Some("actual user request"));
|
|
assert_eq!(metadata.title, "actual user request");
|
|
}
|
|
|
|
#[test]
|
|
fn guardian_projection_skips_prompts_and_preserves_custom_names() {
|
|
let thread_id = ThreadId::new();
|
|
let source = SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string()));
|
|
let mut metadata =
|
|
ThreadMetadataBuilder::new(thread_id, PathBuf::new(), Utc::now(), source.clone())
|
|
.build("test-provider");
|
|
let session_meta = RolloutItem::SessionMeta(SessionMetaLine {
|
|
meta: SessionMeta {
|
|
session_id: thread_id.into(),
|
|
id: thread_id,
|
|
source,
|
|
..Default::default()
|
|
},
|
|
git: None,
|
|
});
|
|
apply_rollout_item(&mut metadata, &session_meta, "test-provider");
|
|
let mut expected = metadata.clone();
|
|
expected.title = super::GUARDIAN_THREAD_TITLE.to_string();
|
|
expected.name = None;
|
|
expected.preview = Some(super::GUARDIAN_THREAD_PREVIEW.to_string());
|
|
expected.first_user_message = None;
|
|
|
|
let user = UserMessageItem::new(&[UserInput::Text {
|
|
text: "large synthetic guardian prompt".to_string(),
|
|
text_elements: Vec::new(),
|
|
}]);
|
|
for event in [
|
|
EventMsg::UserMessage(user.as_legacy_user_message_event()),
|
|
EventMsg::ItemCompleted(ItemCompletedEvent {
|
|
thread_id,
|
|
turn_id: "turn-1".to_string(),
|
|
item: TurnItem::UserMessage(user),
|
|
started_at_ms: Some(0),
|
|
completed_at_ms: 0,
|
|
}),
|
|
] {
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::EventMsg(event),
|
|
"test-provider",
|
|
);
|
|
assert_eq!(metadata, expected);
|
|
}
|
|
|
|
let mut existing = metadata.clone();
|
|
existing.title = "Named Guardian review".to_string();
|
|
metadata.prefer_existing_explicit_title(&existing);
|
|
assert_eq!(metadata, existing);
|
|
|
|
metadata.name = Some("Named Guardian review".to_string());
|
|
let expected = metadata.clone();
|
|
apply_rollout_item(&mut metadata, &session_meta, "test-provider");
|
|
assert_eq!(metadata, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn event_msg_image_only_user_message_sets_image_placeholder_preview() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
client_id: None,
|
|
message: String::new(),
|
|
images: Some(vec!["https://example.com/image.png".to_string()]),
|
|
local_images: vec![],
|
|
text_elements: vec![],
|
|
..Default::default()
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(metadata.first_user_message.as_deref(), Some("[Image]"));
|
|
assert_eq!(metadata.preview.as_deref(), Some("[Image]"));
|
|
assert_eq!(metadata.title, "");
|
|
}
|
|
|
|
#[test]
|
|
fn event_msg_audio_only_user_message_sets_audio_placeholder_preview() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
client_id: None,
|
|
message: String::new(),
|
|
images: None,
|
|
local_images: vec![],
|
|
audio: Some(vec!["https://example.com/audio.mp3".to_string()]),
|
|
local_audio: vec![],
|
|
text_elements: vec![],
|
|
..Default::default()
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(metadata.first_user_message.as_deref(), Some("[Audio]"));
|
|
assert_eq!(metadata.preview.as_deref(), Some("[Audio]"));
|
|
assert_eq!(metadata.title, "");
|
|
}
|
|
|
|
#[test]
|
|
fn event_msg_blank_user_message_without_images_keeps_first_user_message_empty() {
|
|
let mut metadata = metadata_for_test();
|
|
let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
client_id: None,
|
|
message: " ".to_string(),
|
|
images: Some(vec![]),
|
|
local_images: vec![],
|
|
text_elements: vec![],
|
|
..Default::default()
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(metadata.first_user_message, None);
|
|
assert_eq!(metadata.preview, None);
|
|
assert_eq!(metadata.title, "");
|
|
}
|
|
|
|
#[test]
|
|
fn event_msg_thread_goal_sets_preview_only_and_later_user_sets_message_title() {
|
|
let mut metadata = metadata_for_test();
|
|
let goal_item =
|
|
RolloutItem::EventMsg(EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent {
|
|
thread_id: metadata.id,
|
|
turn_id: None,
|
|
goal: ThreadGoal {
|
|
thread_id: metadata.id,
|
|
objective: "optimize the benchmark".to_string(),
|
|
status: ThreadGoalStatus::Active,
|
|
token_budget: None,
|
|
tokens_used: 0,
|
|
time_used_seconds: 0,
|
|
created_at: 1,
|
|
updated_at: 1,
|
|
},
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &goal_item, "test-provider");
|
|
|
|
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
|
assert_eq!(metadata.first_user_message, None);
|
|
assert_eq!(metadata.title, "");
|
|
|
|
let user_item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
client_id: None,
|
|
message: format!("{USER_MESSAGE_BEGIN} next normal prompt"),
|
|
images: Some(vec![]),
|
|
local_images: vec![],
|
|
text_elements: vec![],
|
|
..Default::default()
|
|
}));
|
|
|
|
apply_rollout_item(&mut metadata, &user_item, "test-provider");
|
|
|
|
assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark"));
|
|
assert_eq!(
|
|
metadata.first_user_message.as_deref(),
|
|
Some("next normal prompt")
|
|
);
|
|
assert_eq!(metadata.title, "next normal prompt");
|
|
}
|
|
|
|
#[test]
|
|
fn turn_context_does_not_override_session_cwd() {
|
|
let mut metadata = metadata_for_test();
|
|
metadata.cwd = PathBuf::new();
|
|
let thread_id = metadata.id;
|
|
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::SessionMeta(SessionMetaLine {
|
|
meta: SessionMeta {
|
|
creator_user_id: None,
|
|
creator_account_id: None,
|
|
session_id: thread_id.into(),
|
|
id: thread_id,
|
|
forked_from_id: Some(
|
|
ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"),
|
|
),
|
|
forked_from_ordinal_exclusive: None,
|
|
parent_thread_id: None,
|
|
timestamp: "2026-02-26T00:00:00.000Z".to_string(),
|
|
cwd: PathBuf::from("/child/worktree"),
|
|
runtime_workspace_roots: None,
|
|
originator: "codex_cli_rs".to_string(),
|
|
cli_version: "0.0.0".to_string(),
|
|
source: SessionSource::Cli,
|
|
thread_source: None,
|
|
agent_path: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
model_provider: Some("openai".to_string()),
|
|
base_instructions: None,
|
|
dynamic_tools: None,
|
|
selected_capability_roots: Vec::new(),
|
|
memory_mode: None,
|
|
history_mode: Default::default(),
|
|
history_base: None,
|
|
subagent_history_start_ordinal: None,
|
|
multi_agent_version: None,
|
|
context_window: None,
|
|
},
|
|
git: None,
|
|
}),
|
|
"test-provider",
|
|
);
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::TurnContext(TurnContextItem {
|
|
turn_id: Some("turn-1".to_string()),
|
|
root_turn_id: None,
|
|
disabled_plugin_ids: None,
|
|
cwd: serde_json::from_value(serde_json::json!(
|
|
std::env::current_dir()
|
|
.expect("current directory")
|
|
.join("parent/workspace")
|
|
))
|
|
.expect("absolute parent cwd"),
|
|
approval_policy: AskForApproval::Never,
|
|
approvals_reviewer: None,
|
|
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
|
permission_profile: None,
|
|
active_permission_profile: None,
|
|
file_system_sandbox_policy: None,
|
|
model: "gpt-5".to_string(),
|
|
comp_hash: None,
|
|
collaboration_mode: None,
|
|
multi_agent_version: None,
|
|
realtime_active: None,
|
|
cyber_access_program: None,
|
|
effort: None,
|
|
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
|
|
}),
|
|
"test-provider",
|
|
);
|
|
|
|
assert_eq!(metadata.cwd, PathBuf::from("/child/worktree"));
|
|
let permission_profile: PermissionProfile = PermissionProfile::Disabled;
|
|
assert_eq!(
|
|
metadata.sandbox_policy,
|
|
serde_json::to_string(&permission_profile).expect("serialize permission profile")
|
|
);
|
|
assert_eq!(metadata.approval_mode, "never");
|
|
}
|
|
|
|
#[test]
|
|
fn turn_context_sets_permission_profile_metadata() {
|
|
let mut metadata = metadata_for_test();
|
|
let permission_profile = PermissionProfile::workspace_write();
|
|
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::TurnContext(TurnContextItem {
|
|
turn_id: Some("turn-1".to_string()),
|
|
root_turn_id: None,
|
|
disabled_plugin_ids: None,
|
|
cwd: serde_json::from_value(serde_json::json!(
|
|
std::env::current_dir()
|
|
.expect("current directory")
|
|
.join("workspace")
|
|
))
|
|
.expect("absolute workspace cwd"),
|
|
approval_policy: AskForApproval::OnRequest,
|
|
approvals_reviewer: None,
|
|
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
|
permission_profile: Some(permission_profile.clone()),
|
|
active_permission_profile: None,
|
|
file_system_sandbox_policy: None,
|
|
model: "gpt-5".to_string(),
|
|
comp_hash: None,
|
|
collaboration_mode: None,
|
|
multi_agent_version: None,
|
|
realtime_active: None,
|
|
cyber_access_program: None,
|
|
effort: None,
|
|
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
|
|
}),
|
|
"test-provider",
|
|
);
|
|
|
|
assert_eq!(
|
|
metadata.sandbox_policy,
|
|
serde_json::to_string(&permission_profile).expect("serialize permission profile")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn turn_context_sets_cwd_when_session_cwd_missing() {
|
|
let mut metadata = metadata_for_test();
|
|
metadata.cwd = PathBuf::new();
|
|
let fallback_cwd = std::env::current_dir()
|
|
.expect("current directory")
|
|
.join("fallback/workspace");
|
|
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::TurnContext(TurnContextItem {
|
|
turn_id: Some("turn-1".to_string()),
|
|
root_turn_id: None,
|
|
disabled_plugin_ids: None,
|
|
cwd: serde_json::from_value(serde_json::json!(&fallback_cwd))
|
|
.expect("absolute fallback cwd"),
|
|
approval_policy: AskForApproval::OnRequest,
|
|
approvals_reviewer: None,
|
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
|
permission_profile: None,
|
|
active_permission_profile: None,
|
|
file_system_sandbox_policy: None,
|
|
model: "gpt-5".to_string(),
|
|
comp_hash: None,
|
|
collaboration_mode: None,
|
|
multi_agent_version: None,
|
|
realtime_active: None,
|
|
cyber_access_program: None,
|
|
effort: Some(ReasoningEffort::High),
|
|
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
|
|
}),
|
|
"test-provider",
|
|
);
|
|
|
|
assert_eq!(metadata.cwd, fallback_cwd);
|
|
}
|
|
|
|
#[test]
|
|
fn turn_context_sets_model_and_reasoning_effort() {
|
|
let mut metadata = metadata_for_test();
|
|
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::TurnContext(TurnContextItem {
|
|
turn_id: Some("turn-1".to_string()),
|
|
root_turn_id: None,
|
|
disabled_plugin_ids: None,
|
|
cwd: serde_json::from_value(serde_json::json!(
|
|
std::env::current_dir()
|
|
.expect("current directory")
|
|
.join("fallback/workspace")
|
|
))
|
|
.expect("absolute fallback cwd"),
|
|
approval_policy: AskForApproval::OnRequest,
|
|
approvals_reviewer: None,
|
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
|
permission_profile: None,
|
|
active_permission_profile: None,
|
|
file_system_sandbox_policy: None,
|
|
model: "gpt-5".to_string(),
|
|
comp_hash: None,
|
|
collaboration_mode: None,
|
|
multi_agent_version: None,
|
|
realtime_active: None,
|
|
cyber_access_program: None,
|
|
effort: Some(ReasoningEffort::High),
|
|
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
|
|
}),
|
|
"test-provider",
|
|
);
|
|
|
|
assert_eq!(metadata.model.as_deref(), Some("gpt-5"));
|
|
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High));
|
|
}
|
|
|
|
#[test]
|
|
fn thread_settings_applied_updates_resume_metadata() {
|
|
let mut metadata = metadata_for_test();
|
|
let permission_profile = PermissionProfile::workspace_write();
|
|
let cwd = std::env::current_dir()
|
|
.expect("current directory")
|
|
.join("updated/workspace");
|
|
let item = RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(
|
|
ThreadSettingsAppliedEvent {
|
|
thread_id: None,
|
|
thread_settings: ThreadSettingsSnapshot {
|
|
disabled_plugin_ids: Vec::new(),
|
|
model: "gpt-5.2-codex".to_string(),
|
|
model_provider_id: "updated-provider".to_string(),
|
|
service_tier: None,
|
|
approval_policy: AskForApproval::Never,
|
|
approvals_reviewer: ApprovalsReviewer::User,
|
|
permission_profile: permission_profile.clone(),
|
|
active_permission_profile: None,
|
|
cwd: cwd.clone().try_into().expect("absolute settings cwd"),
|
|
runtime_workspace_roots: None,
|
|
reasoning_effort: Some(ReasoningEffort::Ultra),
|
|
reasoning_summary: Some(ReasoningSummary::Auto),
|
|
personality: None,
|
|
collaboration_mode: CollaborationMode {
|
|
mode: ModeKind::Default,
|
|
settings: Settings {
|
|
model: "gpt-5.2-codex".to_string(),
|
|
reasoning_effort: Some(ReasoningEffort::Ultra),
|
|
developer_instructions: None,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
));
|
|
|
|
assert!(rollout_item_affects_thread_metadata(&item));
|
|
apply_rollout_item(&mut metadata, &item, "test-provider");
|
|
|
|
assert_eq!(metadata.model.as_deref(), Some("gpt-5.2-codex"));
|
|
assert_eq!(metadata.model_provider, "updated-provider");
|
|
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::Ultra));
|
|
assert_eq!(metadata.cwd, cwd);
|
|
assert_eq!(metadata.approval_mode, "never");
|
|
assert_eq!(
|
|
metadata.sandbox_policy,
|
|
serde_json::to_string(&permission_profile)
|
|
.expect("permission profile should serialize")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn session_meta_does_not_set_model_or_reasoning_effort() {
|
|
let mut metadata = metadata_for_test();
|
|
metadata.history_mode = ThreadHistoryMode::Paginated;
|
|
let thread_id = metadata.id;
|
|
|
|
apply_rollout_item(
|
|
&mut metadata,
|
|
&RolloutItem::SessionMeta(SessionMetaLine {
|
|
meta: SessionMeta {
|
|
creator_user_id: None,
|
|
creator_account_id: None,
|
|
session_id: thread_id.into(),
|
|
id: thread_id,
|
|
forked_from_id: None,
|
|
forked_from_ordinal_exclusive: None,
|
|
parent_thread_id: None,
|
|
timestamp: "2026-02-26T00:00:00.000Z".to_string(),
|
|
cwd: PathBuf::from("/workspace"),
|
|
runtime_workspace_roots: None,
|
|
originator: "codex_cli_rs".to_string(),
|
|
cli_version: "0.0.0".to_string(),
|
|
source: SessionSource::Cli,
|
|
thread_source: None,
|
|
agent_path: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
model_provider: Some("openai".to_string()),
|
|
base_instructions: None,
|
|
dynamic_tools: None,
|
|
selected_capability_roots: Vec::new(),
|
|
memory_mode: None,
|
|
history_mode: ThreadHistoryMode::Legacy,
|
|
history_base: None,
|
|
subagent_history_start_ordinal: None,
|
|
multi_agent_version: None,
|
|
context_window: None,
|
|
},
|
|
git: None,
|
|
}),
|
|
"test-provider",
|
|
);
|
|
|
|
assert_eq!(metadata.model, None);
|
|
assert_eq!(metadata.reasoning_effort, None);
|
|
assert_eq!(metadata.history_mode, ThreadHistoryMode::Paginated);
|
|
}
|
|
|
|
fn metadata_for_test() -> ThreadMetadata {
|
|
let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id");
|
|
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_600, 0).expect("timestamp");
|
|
ThreadMetadata {
|
|
creator_user_id: None,
|
|
creator_account_id: None,
|
|
originator: None,
|
|
id,
|
|
rollout_path: PathBuf::from("/tmp/a.jsonl"),
|
|
created_at,
|
|
updated_at: created_at,
|
|
recency_at: created_at,
|
|
source: "cli".to_string(),
|
|
history_mode: Default::default(),
|
|
thread_source: None,
|
|
agent_path: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
model_provider: "openai".to_string(),
|
|
model: None,
|
|
reasoning_effort: None,
|
|
cwd: PathBuf::from("/tmp"),
|
|
cli_version: "0.0.0".to_string(),
|
|
title: String::new(),
|
|
name: None,
|
|
preview: None,
|
|
sandbox_policy: "read-only".to_string(),
|
|
approval_mode: "on-request".to_string(),
|
|
tokens_used: 1,
|
|
first_user_message: None,
|
|
archived_at: None,
|
|
section: None,
|
|
section_position: None,
|
|
section_entered_at: None,
|
|
project_id: None,
|
|
daybreak_enabled: None,
|
|
git_sha: None,
|
|
git_branch: None,
|
|
git_origin_url: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn diff_fields_detects_changes() {
|
|
let mut base = metadata_for_test();
|
|
base.id = ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id");
|
|
base.title = "hello".to_string();
|
|
let mut other = base.clone();
|
|
other.tokens_used = 2;
|
|
other.title = "world".to_string();
|
|
let diffs = base.diff_fields(&other);
|
|
assert_eq!(diffs, vec!["title", "tokens_used"]);
|
|
}
|
|
}
|