- Wire AppendLog as the backing store for Session.messages - Add Deref, From impls, and explicit mutation methods to AppendLog - Narrow API: remove DerefMut, add push_batch/truncate_to/trim_front/clear/last_mut - Update all direct message assignments to use .into() conversions - Update tests to deref through AppendLog for comparisons Rebased onto upstream/main (v0.8.57) to resolve merge conflicts.
This commit is contained in:
@@ -226,7 +226,7 @@ pub fn load(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
}
|
||||
};
|
||||
|
||||
app.api_messages.clone_from(&session.messages);
|
||||
app.api_messages = session.messages.clone().into();
|
||||
app.clear_history();
|
||||
let cells_to_add: Vec<_> = app
|
||||
.api_messages
|
||||
|
||||
@@ -1302,7 +1302,7 @@ impl Engine {
|
||||
} else if messages.is_empty() && system_prompt.is_none() {
|
||||
self.session.id = uuid::Uuid::new_v4().to_string();
|
||||
}
|
||||
self.session.messages = messages;
|
||||
self.session.messages = messages.into();
|
||||
self.session.compaction_summary_prompt =
|
||||
extract_compaction_summary_prompt(system_prompt.clone());
|
||||
self.session.system_prompt = system_prompt;
|
||||
@@ -1351,7 +1351,7 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
if let Some(idx) = cut {
|
||||
self.session.messages.truncate(idx);
|
||||
self.session.messages.truncate_to(idx);
|
||||
self.session.bump_messages_revision();
|
||||
}
|
||||
// Now dispatch the new message as a normal send,
|
||||
@@ -1398,7 +1398,7 @@ impl Engine {
|
||||
.tx_event
|
||||
.send(Event::SessionUpdated {
|
||||
session_id: self.session.id.clone(),
|
||||
messages: self.session.messages.clone(),
|
||||
messages: self.session.messages.clone().into(),
|
||||
system_prompt: self.session.system_prompt.clone(),
|
||||
model: self.session.model.clone(),
|
||||
workspace: self.session.workspace.clone(),
|
||||
@@ -1938,7 +1938,7 @@ impl Engine {
|
||||
Ok(result) => {
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
let messages_after = result.messages.len();
|
||||
self.session.messages = result.messages;
|
||||
self.session.messages = result.messages.into();
|
||||
self.merge_compaction_summary(result.summary_prompt);
|
||||
self.emit_session_updated().await;
|
||||
let removed = messages_before.saturating_sub(messages_after);
|
||||
@@ -2034,7 +2034,7 @@ impl Engine {
|
||||
{
|
||||
Ok(result) => {
|
||||
let messages_after = result.messages.len();
|
||||
self.session.messages = result.messages;
|
||||
self.session.messages = result.messages.into();
|
||||
self.emit_session_updated().await;
|
||||
|
||||
let summary = format!(
|
||||
@@ -2088,7 +2088,7 @@ impl Engine {
|
||||
while self.session.messages.len() > MIN_RECENT_MESSAGES_TO_KEEP
|
||||
&& self.estimated_input_tokens() > target_input_budget
|
||||
{
|
||||
self.session.messages.remove(0);
|
||||
self.session.messages.trim_front(1);
|
||||
self.session.bump_messages_revision();
|
||||
removed = removed.saturating_add(1);
|
||||
}
|
||||
@@ -2110,7 +2110,7 @@ impl Engine {
|
||||
|
||||
let mut retries_used = 0u32;
|
||||
let mut summary_prompt = None;
|
||||
let mut compacted_messages = self.session.messages.clone();
|
||||
let mut compacted_messages: Vec<Message> = self.session.messages.clone().into();
|
||||
|
||||
let mut forced_config = self.config.compaction.clone();
|
||||
forced_config.enabled = true;
|
||||
@@ -2145,7 +2145,7 @@ impl Engine {
|
||||
}
|
||||
|
||||
if !compacted_messages.is_empty() || self.session.messages.is_empty() {
|
||||
self.session.messages = compacted_messages;
|
||||
self.session.messages = compacted_messages.into();
|
||||
}
|
||||
self.merge_compaction_summary(summary_prompt);
|
||||
|
||||
@@ -2219,7 +2219,7 @@ impl Engine {
|
||||
self.session.model.clone(),
|
||||
self.session.workspace.clone(),
|
||||
self.session.system_prompt.clone(),
|
||||
self.session.messages.clone(),
|
||||
self.session.messages.clone().into(),
|
||||
))
|
||||
.with_cancel_token(self.cancel_token.clone())
|
||||
.with_trusted_external_paths(trusted_external_paths);
|
||||
|
||||
@@ -415,7 +415,7 @@ impl Engine {
|
||||
{
|
||||
Ok(result) => {
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
self.session.messages = result.messages;
|
||||
self.session.messages = result.messages.into();
|
||||
self.merge_compaction_summary(result.summary_prompt);
|
||||
refreshed = true;
|
||||
}
|
||||
|
||||
@@ -1799,12 +1799,13 @@ fn runtime_prompt_is_projected_without_persisting_to_session_messages() {
|
||||
text: "summary after compaction".to_string(),
|
||||
cache_control: None,
|
||||
}],
|
||||
}];
|
||||
}]
|
||||
.into();
|
||||
let stored = engine.session.messages.clone();
|
||||
|
||||
let request_messages = engine.messages_with_turn_metadata();
|
||||
|
||||
assert_eq!(engine.session.messages, stored);
|
||||
assert_eq!(&*engine.session.messages, &*stored);
|
||||
assert_eq!(request_messages.len(), stored.len() + 1);
|
||||
assert!(
|
||||
request_messages
|
||||
@@ -2488,7 +2489,7 @@ fn messages_with_turn_metadata_preserves_stored_messages_for_prefix_cache() {
|
||||
let first_request = engine.messages_with_turn_metadata();
|
||||
assert_eq!(
|
||||
&first_request[..engine.session.messages.len()],
|
||||
engine.session.messages.as_slice()
|
||||
&engine.session.messages[..]
|
||||
);
|
||||
assert_eq!(first_request.len(), engine.session.messages.len() + 1);
|
||||
assert_eq!(first_request.first(), Some(&first_user));
|
||||
@@ -2514,7 +2515,7 @@ fn messages_with_turn_metadata_preserves_stored_messages_for_prefix_cache() {
|
||||
let second_request = engine.messages_with_turn_metadata();
|
||||
assert_eq!(
|
||||
&second_request[..engine.session.messages.len()],
|
||||
engine.session.messages.as_slice()
|
||||
&engine.session.messages[..]
|
||||
);
|
||||
assert_eq!(second_request.len(), engine.session.messages.len() + 1);
|
||||
assert_eq!(second_request.first(), Some(&first_user));
|
||||
|
||||
@@ -153,7 +153,7 @@ impl Engine {
|
||||
// Only update if we got valid messages (never corrupt state)
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
let auto_messages_after = result.messages.len();
|
||||
self.session.messages = result.messages;
|
||||
self.session.messages = result.messages.into();
|
||||
self.merge_compaction_summary(result.summary_prompt);
|
||||
self.emit_session_updated().await;
|
||||
let removed = auto_messages_before.saturating_sub(auto_messages_after);
|
||||
@@ -2312,7 +2312,7 @@ impl Engine {
|
||||
// messages. This preserves the stable prefix through all stored
|
||||
// messages while avoiding strict chat templates that only allow
|
||||
// system messages at messages[0].
|
||||
let mut messages = self.session.messages.clone();
|
||||
let mut messages: Vec<Message> = self.session.messages.clone().into();
|
||||
messages.push(self.runtime_prompt_message());
|
||||
messages
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use crate::models::{Message, SystemPrompt, Usage};
|
||||
use crate::prefix_cache::PrefixStabilityManager;
|
||||
use crate::project_context::{ProjectContext, load_project_context_with_parents};
|
||||
use crate::prompt_zones::FrozenPrefix;
|
||||
use crate::prompt_zones::{AppendLog, FrozenPrefix};
|
||||
use crate::tui::approval::ApprovalMode;
|
||||
use crate::working_set::WorkingSet;
|
||||
use std::path::PathBuf;
|
||||
@@ -40,8 +40,8 @@ pub struct Session {
|
||||
/// Persisted summary blocks generated by context compaction.
|
||||
pub compaction_summary_prompt: Option<SystemPrompt>,
|
||||
|
||||
/// Conversation history (API format)
|
||||
pub messages: Vec<Message>,
|
||||
/// Conversation history (API format), backed by AppendLog (#2264).
|
||||
pub messages: AppendLog,
|
||||
|
||||
/// Total tokens used in this session
|
||||
pub total_usage: SessionUsage,
|
||||
@@ -145,7 +145,7 @@ impl Session {
|
||||
system_prompt: None,
|
||||
system_prompt_override: false,
|
||||
compaction_summary_prompt: None,
|
||||
messages: Vec::new(),
|
||||
messages: AppendLog::new(),
|
||||
total_usage: SessionUsage::default(),
|
||||
allow_shell,
|
||||
trust_mode,
|
||||
@@ -179,7 +179,7 @@ impl Session {
|
||||
/// invalidate atomically.
|
||||
#[allow(dead_code)]
|
||||
pub fn replace_messages(&mut self, messages: Vec<Message>) {
|
||||
self.messages = messages;
|
||||
self.messages = messages.into();
|
||||
self.messages_revision = self.messages_revision.saturating_add(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -195,16 +195,17 @@ impl std::fmt::Display for PrefixDrift {
|
||||
|
||||
// ── AppendLog ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Append-only conversation history. Only exposes `push`-style mutations.
|
||||
/// Append-only conversation history. Derefs to `&[Message]` via
|
||||
/// [`Deref`](std::ops::Deref) for transparent read access; mutations go
|
||||
/// through explicit methods (`push`, `truncate_to`, `trim_front`, `clear`)
|
||||
/// whose names make cache impact obvious.
|
||||
///
|
||||
/// **Phase 1 scaffolding** — not yet wired into the engine request path.
|
||||
#[allow(dead_code)]
|
||||
/// Phase 4: backing store for `Session.messages` (#2264).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppendLog {
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AppendLog {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -216,27 +217,53 @@ impl AppendLog {
|
||||
Self { messages }
|
||||
}
|
||||
|
||||
/// Append a message to the log. A single-message push is the cheapest
|
||||
/// mutation for prefix-cache stability — it extends the byte sequence
|
||||
/// without disturbing earlier turns.
|
||||
pub fn push(&mut self, message: Message) {
|
||||
self.messages.push(message);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.messages.len()
|
||||
/// Append multiple messages in one operation (fewer cache-line
|
||||
/// invalidations than repeated `push`).
|
||||
pub fn push_batch(&mut self, batch: Vec<Message>) {
|
||||
self.messages.extend(batch);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.messages.is_empty()
|
||||
/// Truncate to keep only the most recent `new_len` messages.
|
||||
/// Discards older messages (and their prefix-cache contribution)
|
||||
/// from the front.
|
||||
pub fn truncate_to(&mut self, new_len: usize) {
|
||||
self.messages.truncate(new_len);
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Message> {
|
||||
self.messages.iter()
|
||||
/// Remove `count` messages from the front (oldest first).
|
||||
/// Cache-destroying: drops the prefix that earlier turns share.
|
||||
pub fn trim_front(&mut self, count: usize) {
|
||||
if count >= self.messages.len() {
|
||||
self.messages.clear();
|
||||
} else {
|
||||
self.messages.drain(0..count);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all messages. Resets cache state completely.
|
||||
pub fn clear(&mut self) {
|
||||
self.messages.clear();
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the last message, if any.
|
||||
/// Prefer this over `last_mut()` on the inner vec — the name signals
|
||||
/// that only the most recent turn's content is being modified.
|
||||
#[must_use]
|
||||
pub fn as_slice(&self) -> &[Message] {
|
||||
&self.messages
|
||||
pub fn last_mut(&mut self) -> Option<&mut Message> {
|
||||
self.messages.last_mut()
|
||||
}
|
||||
|
||||
/// Consume and return the inner `Vec<Message>`.
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<Message> {
|
||||
self.messages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +273,26 @@ impl Default for AppendLog {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Message>> for AppendLog {
|
||||
fn from(messages: Vec<Message>) -> Self {
|
||||
Self { messages }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AppendLog> for Vec<Message> {
|
||||
fn from(log: AppendLog) -> Self {
|
||||
log.messages
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AppendLog {
|
||||
type Target = Vec<Message>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.messages
|
||||
}
|
||||
}
|
||||
|
||||
// ── TurnScratch ────────────────────────────────────────────────────────
|
||||
|
||||
/// Per-turn ephemeral data. Cleared at every turn boundary.
|
||||
|
||||
Reference in New Issue
Block a user