From 62a141d0b8db5e00a68e42c28b1bf4486b8f85ec Mon Sep 17 00:00:00 2001 From: Justin Gao Date: Mon, 1 Jun 2026 23:34:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refs(#2264):=20Phase=202=20=E2=80=94=20wire?= =?UTF-8?q?=20FrozenPrefix::verify()=20into=20turn=5Floop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a three-zone diagnostic layer alongside the existing PrefixStabilityManager::check_and_update(). On the first turn, freeze the PinnedPrefix baseline; on subsequent turns, verify the current system+tool state against the frozen baseline and log drift via tracing::debug!. Phase 2 is warn-only — no request refusal — auto-re-freezes on drift to keep subsequent turn comparisons meaningful. - Session: add frozen_prefix: Option field - turn_loop: import PinnedPrefix, insert verify block after check_and_update, before MessageRequest construction --- crates/tui/src/core/engine/turn_loop.rs | 29 +++++++++++++++++++++++++ crates/tui/src/core/session.rs | 7 ++++++ 2 files changed, 36 insertions(+) diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 9a3245e16..eda7002cd 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,34 @@ impl Engine { } } + // Three-zone prefix contract (#2264): freeze baseline on first + // turn, verify against frozen baseline on subsequent turns. + // Operates alongside PrefixStabilityManager — this is the + // diagnostic layer that never auto-re-pins (unlike check_and_update). + // Phase 2: warn-only, no request refusal. + let system_text = + crate::prefix_cache::system_prompt_text(self.session.system_prompt.as_ref()); + let current_tools: Vec = active_tools.clone().unwrap_or_default(); + + match &self.session.frozen_prefix { + Some(frozen) => { + if let Err(drift) = frozen.verify(&system_text, ¤t_tools) { + tracing::debug!( + target: "prefix_cache", + "three-zone drift: {drift}" + ); + let pinned = + PinnedPrefix::new(self.session.system_prompt.as_ref(), current_tools); + self.session.frozen_prefix = Some(pinned.freeze()); + } + } + None => { + let pinned = + PinnedPrefix::new(self.session.system_prompt.as_ref(), current_tools); + 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, } } From 9ba6045defbb43a6a14b82cb2dc463654121815e Mon Sep 17 00:00:00 2001 From: Justin Gao Date: Mon, 1 Jun 2026 23:41:06 +0800 Subject: [PATCH 2/2] fix: clarify comment, avoid per-turn tool clone on happy path - Comment: remove 'never auto-re-pins' (it does auto-re-freeze), describe accurately as 'auto-re-freeze on drift' - Perf: use as_deref().unwrap_or_default() to borrow &[Tool] for verify(), only to_vec() when constructing PinnedPrefix --- crates/tui/src/core/engine/turn_loop.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index eda7002cd..ec3939e9a 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -312,29 +312,32 @@ impl Engine { } // Three-zone prefix contract (#2264): freeze baseline on first - // turn, verify against frozen baseline on subsequent turns. - // Operates alongside PrefixStabilityManager — this is the - // diagnostic layer that never auto-re-pins (unlike check_and_update). - // Phase 2: warn-only, no request refusal. + // 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: Vec = active_tools.clone().unwrap_or_default(); + 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, ¤t_tools) { + 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); + 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); + let pinned = PinnedPrefix::new( + self.session.system_prompt.as_ref(), + current_tools.to_vec(), + ); self.session.frozen_prefix = Some(pinned.freeze()); } }