diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 9a3245e16..ec3939e9a 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -6,6 +6,7 @@ //! checkpoints, and loop termination. use super::*; +use crate::prompt_zones::PinnedPrefix; fn loop_guard_block_tool_result(message: String) -> ToolResult { ToolResult::error(message).with_metadata(json!({"loop_guard": "identical_tool_call"})) @@ -310,6 +311,37 @@ impl Engine { } } + // Three-zone prefix contract (#2264): freeze baseline on first + // turn, verify against it on subsequent turns. Operates alongside + // PrefixStabilityManager as an independent diagnostic layer. + // Phase 2: warn-only, auto-re-freeze on drift. + let system_text = + crate::prefix_cache::system_prompt_text(self.session.system_prompt.as_ref()); + let current_tools: &[crate::models::Tool] = active_tools.as_deref().unwrap_or_default(); + + match &self.session.frozen_prefix { + Some(frozen) => { + if let Err(drift) = frozen.verify(&system_text, current_tools) { + tracing::debug!( + target: "prefix_cache", + "three-zone drift: {drift}" + ); + let pinned = PinnedPrefix::new( + self.session.system_prompt.as_ref(), + current_tools.to_vec(), + ); + self.session.frozen_prefix = Some(pinned.freeze()); + } + } + None => { + let pinned = PinnedPrefix::new( + self.session.system_prompt.as_ref(), + current_tools.to_vec(), + ); + self.session.frozen_prefix = Some(pinned.freeze()); + } + } + let request = MessageRequest { model: self.session.model.clone(), messages: self.messages_with_turn_metadata(), diff --git a/crates/tui/src/core/session.rs b/crates/tui/src/core/session.rs index cde29b737..df90caffe 100644 --- a/crates/tui/src/core/session.rs +++ b/crates/tui/src/core/session.rs @@ -6,6 +6,7 @@ use crate::cycle_manager::CycleBriefing; 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::tui::approval::ApprovalMode; use crate::working_set::WorkingSet; use chrono::{DateTime, Utc}; @@ -91,6 +92,11 @@ pub struct Session { /// Tracks the immutable prefix fingerprint and detects drift across turns. /// Set during engine construction; None until the first system prompt assembly. pub prefix_stability: Option, + + /// Three-zone immutable prefix baseline (#2264). Frozen on the first + /// request of the session; verified against the current system+tool + /// state before every subsequent request. None until the first turn. + pub frozen_prefix: Option, } /// Cumulative usage statistics for a session. @@ -166,6 +172,7 @@ impl Session { current_cycle_started: Utc::now(), cycle_briefings: Vec::new(), prefix_stability: None, + frozen_prefix: None, } }