Merge branch 'code-branch-20251006-215207' into main

This commit is contained in:
James Peter
2025-10-07 13:59:43 +10:00
7 changed files with 270 additions and 7 deletions
+11
View File
@@ -2437,6 +2437,17 @@ impl Session {
&compacted.message,
);
}
RolloutItem::Event(recorded_event) => {
if let code_protocol::protocol::EventMsg::UserMessage(user_msg_event) = &recorded_event.msg {
history.push(ResponseItem::Message {
id: Some(recorded_event.id.clone()),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: user_msg_event.message.clone(),
}],
});
}
}
_ => {}
}
}
+11 -5
View File
@@ -2483,8 +2483,12 @@ mod tests {
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(val) => std::env::set_var(self.key, val),
None => std::env::remove_var(self.key),
Some(val) => unsafe {
std::env::set_var(self.key, val)
},
None => unsafe {
std::env::remove_var(self.key)
},
}
}
}
@@ -2562,9 +2566,11 @@ persistence = "none"
let _code_home_guard = EnvVarGuard::new("CODE_HOME");
let _codex_home_guard = EnvVarGuard::new("CODEX_HOME");
std::env::set_var("HOME", legacy_home.path());
std::env::remove_var("CODE_HOME");
std::env::remove_var("CODEX_HOME");
unsafe {
std::env::set_var("HOME", legacy_home.path());
std::env::remove_var("CODE_HOME");
std::env::remove_var("CODEX_HOME");
}
let loaded = Config::load_instructions(Some(code_home.path()));
+24 -2
View File
@@ -7,6 +7,8 @@ use std::path::Path;
use std::path::PathBuf;
use code_protocol::ConversationId;
use code_protocol::models::{ContentItem, ResponseItem};
use code_protocol::protocol::EventMsg as ProtoEventMsg;
use code_protocol::protocol::SessionSource;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -36,7 +38,6 @@ use code_protocol::protocol::RolloutItem;
use code_protocol::protocol::RolloutLine;
use code_protocol::protocol::SessionMeta;
use code_protocol::protocol::SessionMetaLine;
use code_protocol::models::ResponseItem;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct SessionStateSnapshot {}
@@ -325,7 +326,28 @@ impl RolloutRecorder {
items.push(RolloutItem::ResponseItem(item));
}
RolloutItem::Event(ev) => {
items.push(RolloutItem::Event(ev));
match &ev.msg {
ProtoEventMsg::UserMessage(user_msg) => {
let mut content = Vec::new();
content.push(ContentItem::InputText {
text: user_msg.message.clone(),
});
if let Some(images) = &user_msg.images {
for image_url in images {
content.push(ContentItem::InputImage {
image_url: image_url.clone(),
});
}
}
items.push(RolloutItem::ResponseItem(ResponseItem::Message {
id: Some(ev.id.clone()),
role: "user".to_string(),
content,
}));
}
ProtoEventMsg::AgentMessage(_) => items.push(RolloutItem::Event(ev)),
_ => items.push(RolloutItem::Event(ev)),
}
}
RolloutItem::Compacted(compacted) => {
items.push(RolloutItem::Compacted(compacted));
+129
View File
@@ -13,11 +13,13 @@ use time::format_description::FormatItem;
use time::macros::format_description;
use uuid::Uuid;
use crate::config::{Config, ConfigOverrides, ConfigToml};
use crate::rollout::INTERACTIVE_SESSION_SOURCES;
use crate::rollout::list::ConversationsPage;
use crate::rollout::list::Cursor;
use crate::rollout::list::get_conversation;
use crate::rollout::list::get_conversations;
use code_protocol::models::{ContentItem, ResponseItem};
use code_protocol::ConversationId;
use code_protocol::protocol::{
EventMsg as ProtoEventMsg,
@@ -32,6 +34,20 @@ use code_protocol::protocol::{
const NO_SOURCE_FILTER: &[SessionSource] = &[];
fn reconstruct_history_like_rollout(items: &[RolloutItem]) -> Vec<ResponseItem> {
let mut history = Vec::new();
for item in items {
match item {
RolloutItem::ResponseItem(response) => history.push(response.clone()),
RolloutItem::Compacted(compacted) => {
history.push(ResponseItem::from(compacted.clone()));
}
_ => {}
}
}
history
}
fn assert_page_summary(
page: &ConversationsPage,
expected_items: &[(PathBuf, &str)],
@@ -142,6 +158,119 @@ fn write_session_file(
Ok((dt, uuid))
}
#[tokio::test]
async fn test_resume_reconstruct_history_drops_user_events() {
let temp = TempDir::new().unwrap();
let code_home = temp.path();
let workspace = code_home.join("workspace");
fs::create_dir_all(&workspace).unwrap();
let sessions_dir = code_home
.join("sessions")
.join("2025")
.join("10")
.join("06");
fs::create_dir_all(&sessions_dir).unwrap();
let rollout_ts = "2025-10-06T09-00-00";
let session_uuid = Uuid::from_u128(0xfeed_babe_u128);
let rollout_path = sessions_dir.join(format!(
"rollout-{rollout_ts}-{session_uuid}.jsonl"
));
let mut writer = BufWriter::new(File::create(&rollout_path).unwrap());
let session_meta = SessionMeta {
id: ConversationId::from(session_uuid),
timestamp: "2025-10-06T09:00:00.000Z".to_string(),
cwd: workspace.clone(),
originator: "regression-test".to_string(),
cli_version: "0.0.0-test".to_string(),
instructions: Some("explain async/await".to_string()),
source: SessionSource::Cli,
};
serde_json::to_writer(&mut writer, &RolloutLine {
timestamp: session_meta.timestamp.clone(),
item: RolloutItem::SessionMeta(SessionMetaLine {
meta: session_meta,
git: None,
}),
})
.unwrap();
writer.write_all(b"\n").unwrap();
let user_event = RecordedEvent {
id: "evt-user-0".to_string(),
event_seq: 0,
order: None,
msg: ProtoEventMsg::UserMessage(UserMessageEvent {
message: "hi there".to_string(),
kind: None,
images: None,
}),
};
serde_json::to_writer(&mut writer, &RolloutLine {
timestamp: "2025-10-06T09:00:01.000Z".to_string(),
item: RolloutItem::Event(user_event),
})
.unwrap();
writer.write_all(b"\n").unwrap();
let assistant_response = ResponseItem::Message {
id: Some("msg-assistant-0".to_string()),
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "Sure, let's talk about async/await.".to_string(),
}],
};
serde_json::to_writer(&mut writer, &RolloutLine {
timestamp: "2025-10-06T09:00:02.000Z".to_string(),
item: RolloutItem::ResponseItem(assistant_response),
})
.unwrap();
writer.write_all(b"\n").unwrap();
writer.flush().unwrap();
let mut overrides = ConfigOverrides::default();
overrides.cwd = Some(workspace.clone());
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
overrides,
code_home.to_path_buf(),
)
.unwrap();
let (recorder, saved) = crate::rollout::RolloutRecorder::resume(&config, &rollout_path)
.await
.expect("resume should succeed");
recorder.shutdown().await.unwrap();
assert_eq!(
saved.items.len(),
3,
"expected to load session meta + user event + assistant response"
);
let reconstructed = reconstruct_history_like_rollout(&saved.items);
assert_eq!(
reconstructed.len(),
2,
"expected replay history to surface both user and assistant messages"
);
let user_messages = reconstructed
.iter()
.filter(|item| matches!(item, ResponseItem::Message { role, .. } if role == "user"))
.count();
assert_eq!(
user_messages,
1,
"expected one user message to survive resume replay"
);
}
#[tokio::test]
async fn test_list_conversations_latest_first() {
let temp = TempDir::new().unwrap();
+3
View File
@@ -263,6 +263,9 @@ fn parse_rollout_candidate(path: &Path, target_cwd: &Path) -> Option<ResumeCandi
if let Some(snippet) = extract_user_snippet_from_event(&event) {
last_user_snippet = Some(snippet);
}
if matches!(event, EventMsg::UserMessage(_)) {
message_count = message_count.saturating_add(1);
}
}
}
RolloutItem::Compacted(_) | RolloutItem::TurnContext(_) => {}
+2
View File
@@ -1,2 +1,4 @@
pub mod discovery;
#[cfg(test)]
mod tests;
+90
View File
@@ -0,0 +1,90 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::Path;
use tempfile::TempDir;
use uuid::Uuid;
use code_protocol::ConversationId;
use code_protocol::protocol::{
EventMsg as ProtoEventMsg,
RecordedEvent,
RolloutItem,
RolloutLine,
SessionMeta,
SessionMetaLine,
SessionSource,
UserMessageEvent,
};
fn write_event_only_session(path: &Path, cwd: &Path) {
let file = File::create(path).unwrap();
let mut writer = BufWriter::new(file);
let session_meta = SessionMeta {
id: ConversationId::from(Uuid::from_u128(0xDEAD_BEEF_u128)),
timestamp: "2025-10-06T12:00:00.000Z".to_string(),
cwd: cwd.to_path_buf(),
originator: "resume-test".to_string(),
cli_version: "0.0.0-test".to_string(),
instructions: None,
source: SessionSource::Cli,
};
let meta_line = RolloutLine {
timestamp: "2025-10-06T12:00:00.000Z".to_string(),
item: RolloutItem::SessionMeta(SessionMetaLine {
meta: session_meta,
git: None,
}),
};
serde_json::to_writer(&mut writer, &meta_line).unwrap();
writer.write_all(b"\n").unwrap();
let user_event = RecordedEvent {
id: "evt-user".to_string(),
event_seq: 1,
order: None,
msg: ProtoEventMsg::UserMessage(UserMessageEvent {
message: "restore me".to_string(),
kind: None,
images: None,
}),
};
let event_line = RolloutLine {
timestamp: "2025-10-06T12:00:01.000Z".to_string(),
item: RolloutItem::Event(user_event),
};
serde_json::to_writer(&mut writer, &event_line).unwrap();
writer.write_all(b"\n").unwrap();
writer.flush().unwrap();
}
#[test]
fn event_only_sessions_are_dropped_by_resume_discovery() {
let temp = TempDir::new().unwrap();
let code_home = temp.path();
let project_cwd = code_home.join("project");
fs::create_dir_all(&project_cwd).unwrap();
let sessions_dir = code_home
.join("sessions")
.join("2025")
.join("10")
.join("06");
fs::create_dir_all(&sessions_dir).unwrap();
let rollout_path = sessions_dir.join("rollout-2025-10-06T12-00-00-00000000-0000-0000-0000-000000000042.jsonl");
write_event_only_session(&rollout_path, &project_cwd);
let results = super::discovery::list_sessions_for_cwd(&project_cwd, code_home);
assert!(
!results.is_empty(),
"expected resume discovery to surface sessions that only contain Event-based user messages. \
An empty result demonstrates the regression where event-only transcripts are filtered out, \
making '/resume' miss recent conversations."
);
}