From 002440b48afbfeb9e89c6b4271b44b70cffb77af Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 28 Apr 2026 06:21:26 +1200 Subject: [PATCH] fix(goose): exclude preprompt from session title generation (#8793) Co-authored-by: Claude Opus 4.6 (1M context) --- crates/goose/src/providers/base.rs | 46 ++++++++++++++++++++++-- crates/goose/src/providers/cli_common.rs | 6 +++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 6b6a0707b9..acedf62080 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -731,16 +731,44 @@ pub trait Provider: Send + Sync { )) } - /// Returns the first 3 user messages as strings for session naming + /// Returns the first 3 user messages as strings for session naming, + /// filtering out assistant-only content (e.g. preprompt blocks). fn get_initial_user_messages(&self, messages: &Conversation) -> Vec { messages .iter() .filter(|m| m.role == rmcp::model::Role::User) .take(MSG_COUNT_FOR_SESSION_NAME_GENERATION) - .map(|m| m.as_concat_text()) + .map(|m| { + m.content + .iter() + .filter_map(|c| c.filter_for_audience(rmcp::model::Role::User)) + .filter_map(|c| c.as_text().map(|s| s.to_string())) + .collect::>() + .join("\n") + }) .collect() } + /// Extracts preprompt context (assistant-audience blocks) from the first user message. + /// These are content blocks visible to the assistant but not the user. + fn get_preprompt_context(&self, messages: &Conversation) -> String { + messages + .iter() + .filter(|m| m.role == rmcp::model::Role::User) + .take(1) + .flat_map(|m| m.content.iter()) + .filter_map(|c| { + // If this block is NOT visible to the user, it's preprompt/assistant-only content + if c.filter_for_audience(rmcp::model::Role::User).is_none() { + c.as_text().map(|s| s.to_string()) + } else { + None + } + }) + .collect::>() + .join("\n") + } + /// Generate a session name/description based on the conversation history /// Creates a prompt asking for a concise description in 4 words or less. async fn generate_session_name( @@ -749,6 +777,7 @@ pub trait Provider: Send + Sync { messages: &Conversation, ) -> Result { let context = self.get_initial_user_messages(messages); + let preprompt_context = self.get_preprompt_context(messages); let system = crate::prompt_template::render_template( "session_name.md", &std::collections::HashMap::::new(), @@ -758,8 +787,19 @@ pub trait Provider: Send + Sync { use super::cli_common::{ SESSION_NAME_BEGIN_MARKER, SESSION_NAME_END_MARKER, SESSION_NAME_SUFFIX, }; + + let preprompt_section = if preprompt_context.is_empty() { + String::new() + } else { + format!( + "---BEGIN BACKGROUND CONTEXT (for understanding only, do NOT base the title on this)---\n{}\n---END BACKGROUND CONTEXT---\n\n", + preprompt_context + ) + }; + let user_text = format!( - "{}\n{}\n{}\n\n{}", + "{}{}\n{}\n{}\n\n{}", + preprompt_section, SESSION_NAME_BEGIN_MARKER, context.join("\n"), SESSION_NAME_END_MARKER, diff --git a/crates/goose/src/providers/cli_common.rs b/crates/goose/src/providers/cli_common.rs index 8118aa06cf..f2531ec9f5 100644 --- a/crates/goose/src/providers/cli_common.rs +++ b/crates/goose/src/providers/cli_common.rs @@ -56,7 +56,11 @@ pub(crate) fn generate_simple_session_description( }) .map(|text| { // Strip the wrapper added by generate_session_name so we get - // the actual user content. + // the actual user content. First strip the optional background context section. + let text = text + .rfind(SESSION_NAME_BEGIN_MARKER) + .and_then(|idx| text.get(idx..)) + .unwrap_or(text); let stripped = text .strip_prefix(SESSION_NAME_BEGIN_MARKER) .unwrap_or(text)