Bump version to 1.6.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Ward
2026-06-12 14:17:06 -05:00
co-authored by Claude Opus 4.6
parent 4ba9706e35
commit 59cfd0e2f5
152 changed files with 8276 additions and 1664 deletions
+500 -114
View File
@@ -73,7 +73,7 @@ use itertools::Itertools;
use parking_lot::FairMutex;
use pending_response_streams::PendingResponseStreams;
use session_sharing_protocol::common::ParticipantId;
use std::collections::{HashMap, HashSet};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::time::Duration;
use warp_multi_agent_api::{message, Task, ToolType};
@@ -168,6 +168,60 @@ pub enum BlocklistAIControllerEvent {
FreeTierLimitCheckTriggered,
}
/// Tracks recent failed action signatures for loop detection.
/// When the same tool+input pattern fails repeatedly, we inject
/// corrective instructions to break the cycle.
#[derive(Debug, Clone)]
struct LoopDetectionEntry {
/// Discriminant of the action result type (e.g. RequestCommandOutput, ApplyFileDiffs)
tool_discriminant: std::mem::Discriminant<AIAgentActionResultType>,
/// Hash of the action's identifying input (command string, file paths, etc.)
input_hash: u64,
/// Human-readable description of what failed
description: String,
}
#[derive(Debug, Default, Clone)]
struct LoopDetectionState {
recent_failures: VecDeque<LoopDetectionEntry>,
}
const LOOP_DETECTION_WINDOW: usize = 10;
const LOOP_DETECTION_THRESHOLD: usize = 3;
impl LoopDetectionState {
fn record_failure(&mut self, entry: LoopDetectionEntry) {
self.recent_failures.push_back(entry);
if self.recent_failures.len() > LOOP_DETECTION_WINDOW {
self.recent_failures.pop_front();
}
}
fn detect_loop(&self) -> Option<&LoopDetectionEntry> {
use std::collections::HashMap as CountMap;
let mut counts: CountMap<
(std::mem::Discriminant<AIAgentActionResultType>, u64),
(usize, usize),
> = CountMap::new();
for (idx, entry) in self.recent_failures.iter().enumerate() {
let key = (entry.tool_discriminant, entry.input_hash);
let counter = counts.entry(key).or_insert((0, 0));
counter.0 += 1;
counter.1 = idx; // Track most recent occurrence
}
for ((_disc, _hash), (count, latest_idx)) in &counts {
if *count >= LOOP_DETECTION_THRESHOLD {
return self.recent_failures.get(*latest_idx);
}
}
None
}
fn clear(&mut self) {
self.recent_failures.clear();
}
}
#[derive(Debug)]
pub struct RequestInput {
pub conversation_id: AIConversationId,
@@ -317,6 +371,9 @@ pub struct BlocklistAIController {
pending_auto_resume_handles: HashMap<AIConversationId, SpawnedFutureHandle>,
/// Passive conversations explicitly requested to follow up after actions complete.
pending_passive_follow_ups: HashSet<AIConversationId>,
/// Per-conversation loop detection state for preventing recursive tool failures.
loop_detection: HashMap<AIConversationId, LoopDetectionState>,
/// Passive suggestion results that should be included with the next request
/// for a given conversation (e.g. accepted/iterated code diffs that weren't
/// auto-resumed).
@@ -555,6 +612,7 @@ impl BlocklistAIController {
pending_auto_resume_handles: HashMap::new(),
pending_passive_follow_ups: HashSet::new(),
pending_passive_suggestion_results: HashMap::new(),
loop_detection: HashMap::new(),
}
}
@@ -1026,6 +1084,9 @@ impl BlocklistAIController {
is_queued_prompt: bool,
ctx: &mut ModelContext<Self>,
) {
// User sending a new query resets loop detection — fresh context.
self.loop_detection.remove(&conversation_id);
let is_viewer = self
.terminal_model
.lock()
@@ -1418,6 +1479,9 @@ impl BlocklistAIController {
return;
}
// Loop detection: record failures and check for repeated patterns
let loop_warning = self.check_and_record_loop_detection(conversation_id, &finished_results);
// Check whether any result will trigger a server-side subagent (e.g. CLI
// subagent for LRC), or if one is already active. If so, we must not
// piggyback orchestration events because the subagent cannot interpret
@@ -1447,6 +1511,34 @@ impl BlocklistAIController {
ctx,
);
// If a loop was detected, inject a corrective instruction alongside
// the action results so the model avoids repeating the same failure.
if let Some(warning_msg) = loop_warning {
log::warn!(
"[loop-detection] Injecting corrective instruction for conversation {:?}: {}",
conversation_id,
warning_msg
);
if let Some(conversation) =
BlocklistAIHistoryModel::as_ref(ctx).conversation(&conversation_id)
{
let root_task_id = conversation.get_root_task_id().clone();
request_input
.input_messages
.entry(root_task_id)
.or_default()
.push(AIAgentInput::UserQuery {
query: warning_msg,
context: Arc::from([]),
static_query_type: None,
referenced_attachments: HashMap::new(),
user_query_mode: UserQueryMode::Normal,
running_command: None,
intended_agent: None,
});
}
}
// Include any pending orchestration events in this follow-up rather
// than waiting for a separate idle injection turn. Skip when a server
// subagent is or will be active — events will be delivered via the idle
@@ -1495,6 +1587,67 @@ impl BlocklistAIController {
self.pending_passive_follow_ups.remove(&conversation_id);
}
/// Records failed actions into the loop detection state and returns a
/// corrective instruction if a loop is detected.
fn check_and_record_loop_detection(
&mut self,
conversation_id: AIConversationId,
results: &[AIAgentActionResult],
) -> Option<String> {
use std::hash::{Hash, Hasher};
let state = self.loop_detection.entry(conversation_id).or_default();
let mut has_success = false;
for result in results {
if result.result.is_failed() {
let discriminant = std::mem::discriminant(&result.result);
// Use a stable description that includes the tool type and the *input*
// (command, file paths, etc.) but NOT the variable output, so the same
// failing command with different output is still recognized as a loop.
let description = result.result.loop_description();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
discriminant.hash(&mut hasher);
description.hash(&mut hasher);
let input_hash = hasher.finish();
state.record_failure(LoopDetectionEntry {
tool_discriminant: discriminant,
input_hash,
description: description.clone(),
});
} else if result.result.is_successful() {
has_success = true;
}
}
// If we had at least one success in this batch, clear loop state —
// the agent is making progress.
if has_success {
state.clear();
return None;
}
// Check for loops
if let Some(looping_entry) = state.detect_loop() {
let warning = format!(
"[SYSTEM] Loop detected: the same action has failed {} or more times consecutively. \
Do NOT repeat this action or any similar approach.\n\n\
Failing action: {}\n\n\
Take a completely different approach to accomplish the goal. \
If you cannot find an alternative, explain to the user what is failing and why.",
LOOP_DETECTION_THRESHOLD,
looping_entry.description
);
// Clear the state so we don't keep injecting on every subsequent turn
state.clear();
Some(warning)
} else {
None
}
}
/// Handles the EventsReady signal. Checks readiness, drains
/// pending events from the service, and injects them into the conversation.
fn handle_pending_events_ready(
@@ -1566,9 +1719,8 @@ impl BlocklistAIController {
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
let events = OrchestrationEventService::handle(ctx).update(ctx, |svc, _ctx| {
svc.drain_subagent_events(&conversation_id)
});
let events = OrchestrationEventService::handle(ctx)
.update(ctx, |svc, _ctx| svc.drain_subagent_events(&conversation_id));
for event in events {
match event.detail {
@@ -1577,17 +1729,16 @@ impl BlocklistAIController {
question_text,
options,
} => {
let answer = options.first().cloned().unwrap_or_else(|| {
format!("Proceed with: {}", question_text)
});
let answer = options
.first()
.cloned()
.unwrap_or_else(|| format!("Proceed with: {}", question_text));
OrchestrationEventService::handle(ctx).update(ctx, |svc, ctx| {
svc.route_answer_to_subagent(source_conversation_id, answer, ctx);
});
}
PendingEventDetail::SubagentAnswer {
answer_text,
} => {
PendingEventDetail::SubagentAnswer { answer_text } => {
self.complete_ask_user_question_with_answer(answer_text, ctx);
}
PendingEventDetail::SubagentCompletionSummary => {}
@@ -1604,7 +1755,10 @@ impl BlocklistAIController {
) {
use ai::agent::action_result::AskUserQuestionAnswerItem;
let executor = self.action_model.as_ref(ctx).ask_user_question_executor(ctx);
let executor = self
.action_model
.as_ref(ctx)
.ask_user_question_executor(ctx);
let answer_item = AskUserQuestionAnswerItem::Answered {
question_id: String::new(),
selected_options: vec![answer_text.clone()],
@@ -1963,7 +2117,8 @@ impl BlocklistAIController {
parent_agent_id,
agent_name,
bedrock_history,
bedrock_compact_summary,
bedrock_tool_result_archive,
bedrock_progressive_summary,
) = {
let Some(conversation) = history_model
.as_ref(ctx)
@@ -1987,7 +2142,8 @@ impl BlocklistAIController {
conversation.parent_agent_id().map(str::to_string),
conversation.agent_name().map(str::to_string),
conversation.bedrock_message_history().to_vec(),
conversation.compact_summary().map(str::to_string),
conversation.tool_result_archive().to_vec(),
conversation.progressive_summary().map(str::to_string),
)
};
@@ -2055,11 +2211,8 @@ impl BlocklistAIController {
request_params.parent_agent_id = parent_agent_id;
request_params.agent_name = agent_name;
request_params.bedrock_message_history = bedrock_history;
request_params.bedrock_compact_summary = bedrock_compact_summary;
request_params.is_summarization = request_input
.all_inputs()
.any(|input| matches!(input, AIAgentInput::SummarizeConversation { .. }));
request_params.bedrock_tool_result_archive = bedrock_tool_result_archive;
request_params.bedrock_progressive_summary = bedrock_progressive_summary;
let server_conversation_token_for_identifiers =
conversation_data.server_conversation_token.clone();
@@ -2084,13 +2237,9 @@ impl BlocklistAIController {
let input_contains_user_query = request_input
.all_inputs()
.any(|input| input.is_user_query());
let input_is_summarization = request_input
.all_inputs()
.any(|input| matches!(input, AIAgentInput::SummarizeConversation { .. }));
ctx.subscribe_to_model(&response_stream, move |me, event, ctx| {
me.handle_response_stream_event(
input_contains_user_query,
input_is_summarization,
event,
&response_stream_clone,
ctx,
@@ -2280,7 +2429,6 @@ impl BlocklistAIController {
fn handle_response_stream_event(
&mut self,
did_input_contain_user_query: bool,
is_summarization_request: bool,
event: &ResponseStreamEvent,
response_stream: &ModelHandle<ResponseStream>,
ctx: &mut ModelContext<Self>,
@@ -2384,69 +2532,32 @@ impl BlocklistAIController {
Some(sent.clone())
}
});
if let Some(new_history) = new_history {
if let Some(mut new_history) = new_history {
let history_model = BlocklistAIHistoryModel::handle(ctx);
history_model.update(ctx, |history_model, _| {
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
// If this was a summarization request, compact the
// history to just the summary instead of keeping
// the full message list. This is what actually
// frees up context window space.
let is_summarization = is_summarization_request;
if is_summarization {
// Extract the assistant's summary from the last
// message in the history (the response).
let summary_text = new_history
if let Some(conversation) =
history_model.conversation_mut(&conversation_id)
{
let skip = conversation.messages_summarized_up_to();
if skip > 0 && skip <= new_history.len() {
let drained: Vec<_> = new_history
.iter()
.rev()
.find_map(|msg| {
use crate::ai::bedrock::convert::{
MessageContent, MessageRole,
};
if msg.role == MessageRole::Assistant {
if let MessageContent::Text(text) =
&msg.content
{
Some(text.clone())
} else {
None
}
} else {
None
}
});
if let Some(summary) = summary_text {
log::info!(
"[bedrock] Compacted conversation history from {} messages to system-level summary",
new_history.len()
);
conversation.set_compact_summary(Some(summary.clone()));
*conversation.bedrock_message_history_mut() = Vec::new();
let estimated_tokens = (summary.len() / 4) as u32;
let max_context = crate::ai::bedrock::response_translator::context_window_for_model("claude-opus-4-6-20250514[1m]");
let new_usage = estimated_tokens as f32 / max_context as f32;
conversation.set_context_window_usage(new_usage);
conversation.set_current_context_tokens(estimated_tokens);
log::info!(
"[bedrock] Post-compact context estimate: ~{} tokens ({:.1}% of context window)",
estimated_tokens,
new_usage * 100.0
);
} else {
*conversation.bedrock_message_history_mut() =
new_history;
}
.take(skip)
.cloned()
.collect();
conversation.archive_tool_results(drained);
let reconciled = new_history.split_off(skip);
conversation.reset_messages_summarized_up_to();
*conversation.bedrock_message_history_mut() =
reconciled;
} else {
*conversation.bedrock_message_history_mut() =
new_history;
log::info!(
"[bedrock] Updated conversation bedrock history: {} messages",
conversation.bedrock_message_history().len()
);
}
log::info!(
"[bedrock] Updated conversation bedrock history: {} messages",
conversation.bedrock_message_history().len()
);
}
});
}
@@ -2488,22 +2599,24 @@ impl BlocklistAIController {
});
}
let mut renderable_error: RenderableAIError =
if let AIApiError::Stream { stream_type, source } = e.as_ref() {
if *stream_type == "bedrock_converse"
&& is_bedrock_credentials_error(&source.to_string())
{
let model_name =
response_stream.as_ref(ctx).model_id().to_string();
RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid {
model_name,
}
} else {
e.as_ref().into()
let mut renderable_error: RenderableAIError = if let AIApiError::Stream {
stream_type,
source,
} = e.as_ref()
{
if *stream_type == "bedrock_converse"
&& is_bedrock_credentials_error(&source.to_string())
{
let model_name = response_stream.as_ref(ctx).model_id().to_string();
RenderableAIError::AwsBedrockCredentialsExpiredOrInvalid {
model_name,
}
} else {
e.as_ref().into()
};
}
} else {
e.as_ref().into()
};
if let RenderableAIError::Other {
will_attempt_resume,
waiting_for_network,
@@ -2555,7 +2668,9 @@ impl BlocklistAIController {
log::warn!("Conversation not found.");
return;
};
let new_exchange_ids: Vec<_> = conversation.new_exchange_ids_for_response(&stream_id).collect();
let new_exchange_ids: Vec<_> = conversation
.new_exchange_ids_for_response(&stream_id)
.collect();
log::info!(
"[bedrock-debug] AfterStreamFinished: stream_id={:?}, conversation_id={:?}, new_exchange_ids count={}",
stream_id, conversation_id, new_exchange_ids.len()
@@ -2963,42 +3078,311 @@ impl BlocklistAIController {
ctx.emit(BlocklistAIControllerEvent::FreeTierLimitCheckTriggered);
}
// Auto-compact: trigger summarization when context window usage >= 85%.
let should_auto_compact = {
// Progressive summarization: when context window usage >= 85% and we have
// more than 100 messages, summarize the oldest messages while keeping the
// most recent 100 verbatim. This runs as a background Bedrock call — no UI,
// no exchange created, no tool execution shown.
let should_progressive_summarize = {
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
history_model
.conversation(&conversation_id)
.is_some_and(|conversation| {
let is_summarization_request = conversation
.latest_exchange()
.is_some_and(|exchange| {
let is_summarization_request =
conversation.latest_exchange().is_some_and(|exchange| {
exchange
.input
.iter()
.any(|i| matches!(i, AIAgentInput::SummarizeConversation { .. }))
});
conversation.context_window_usage() >= 0.85
&& !conversation.has_pending_auto_compact()
&& !conversation.has_pending_progressive_summary()
&& !is_summarization_request
&& conversation.bedrock_message_history().len() > 100
})
};
if should_auto_compact {
log::info!(
"[auto-compact] Context window usage >= 85% for conversation {:?}, triggering summarization",
conversation_id
);
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _| {
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
conversation.set_has_pending_auto_compact(true);
}
});
self.send_slash_command_request(
SlashCommandRequest::Summarize { prompt: None },
ctx,
);
if should_progressive_summarize {
self.trigger_progressive_summarization(conversation_id, ctx);
}
}
fn trigger_progressive_summarization(
&mut self,
conversation_id: AIConversationId,
ctx: &mut ModelContext<Self>,
) {
use crate::ai::bedrock::client::{BedrockClient, BedrockClientConfig};
use crate::ai::bedrock::convert::{ConversationMessage, MessageContent, MessageRole};
use crate::ai::bedrock::response_translator::{
context_window_for_model, estimate_cost_cents,
};
use crate::settings::ai::AISettings;
use settings::Setting;
let settings = AISettings::as_ref(ctx);
if !*settings.bedrock_enabled.value() {
return;
}
let config = BedrockClientConfig {
auth_method: *settings.bedrock_auth_method.value(),
profile: settings.bedrock_profile.value().clone(),
region: settings.bedrock_region.value().clone(),
access_key_id: settings.bedrock_access_key_id.value().clone(),
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
}
.with_external_fallbacks();
let cross_region = config.cross_region_inference;
// Use Sonnet for summarization — cheaper and fast enough for this task
let model_id = "us.anthropic.claude-sonnet-4-6-20250514-v1:0".to_string();
let history_model = BlocklistAIHistoryModel::handle(ctx);
// Extract the messages to summarize and set the guard flag
let (messages_to_summarize, existing_summary, messages_count) = {
let history = history_model.as_ref(ctx);
let Some(conversation) = history.conversation(&conversation_id) else {
return;
};
let history_len = conversation.bedrock_message_history().len();
let split_point = history_len.saturating_sub(100);
if split_point == 0 {
return;
}
let msgs: Vec<ConversationMessage> =
conversation.bedrock_message_history()[..split_point].to_vec();
let existing = conversation.progressive_summary().map(str::to_string);
(msgs, existing, split_point)
};
history_model.update(ctx, |history_model, _| {
if let Some(conversation) = history_model.conversation_mut(&conversation_id) {
conversation.set_has_pending_progressive_summary(true);
}
});
log::info!(
"[progressive-summary] Triggering for conversation {:?}: summarizing {} messages, keeping last 100",
conversation_id,
messages_count
);
// Build the summarization input
let mut summarize_content = String::new();
if let Some(ref prior) = existing_summary {
summarize_content.push_str("<prior-summary>\n");
summarize_content.push_str(prior);
summarize_content.push_str("\n</prior-summary>\n\n");
}
summarize_content.push_str("<messages-to-summarize>\n");
fn safe_truncate(s: &str, max_chars: usize) -> String {
if s.len() <= max_chars {
s.to_string()
} else {
let trunc = s.chars().take(max_chars).collect::<String>();
format!("{trunc}... [truncated, {len} total chars]", len = s.len())
}
}
for msg in &messages_to_summarize {
let role_str = match msg.role {
MessageRole::User => "User",
MessageRole::Assistant => "Assistant",
};
let content_str = match &msg.content {
MessageContent::Text(t) => t.clone(),
MessageContent::ToolUse { name, input, .. } => {
format!("[Tool Call: {}] {}", name, input)
}
MessageContent::ToolResult { content, .. } => safe_truncate(content, 2000),
MessageContent::MultiPart(parts) => {
use crate::ai::bedrock::convert::ContentPart;
parts
.iter()
.map(|p| match p {
ContentPart::Text(t) => t.clone(),
ContentPart::ToolUse { name, input, .. } => {
format!("[Tool: {}] {}", name, input)
}
ContentPart::ToolResult { content, .. } => safe_truncate(content, 2000),
})
.collect::<Vec<_>>()
.join("\n")
}
};
summarize_content.push_str(&format!("[{}]: {}\n", role_str, content_str));
}
summarize_content.push_str("</messages-to-summarize>");
let summarize_prompt = "Summarize the following conversation history. Preserve:\n\
- All decisions made and their rationale\n\
- All file paths modified and what was changed\n\
- All tool calls with their significant results (commands run, files read, errors encountered)\n\
- Current task state and any pending work\n\
- Technical details, code patterns, and architecture discussed\n\n\
Be comprehensive. This summary will be the only record of these exchanges.";
let summarize_messages = vec![ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!("{}\n\n{}", summarize_prompt, summarize_content)),
}];
// Spawn the background Bedrock call
let model_id_clone = model_id.clone();
ctx.spawn(
async move {
let client = BedrockClient::from_config(config).await?;
client
.converse_collect(
&model_id_clone,
summarize_messages,
None,
16000,
cross_region,
)
.await
},
move |me, result, ctx| {
let history_model = BlocklistAIHistoryModel::handle(ctx);
match result {
Ok((summary_text, input_tokens, output_tokens)) => {
log::info!(
"[progressive-summary] Completed for {:?}: {} chars, input={} output={} tokens",
conversation_id,
summary_text.len(),
input_tokens,
output_tokens,
);
let cost_cents = estimate_cost_cents(
input_tokens,
output_tokens,
0,
0,
&model_id,
);
// Use the conversation's active model for context window sizing,
// not the summarizer model.
let active_model_id = crate::ai::llms::LLMPreferences::as_ref(ctx)
.get_active_base_model(ctx, Some(me.terminal_view_id))
.id
.to_string();
history_model.update(ctx, |history_model, _| {
if let Some(conversation) =
history_model.conversation_mut(&conversation_id)
{
// Drain the summarized messages from history
let drain_count =
messages_count.min(conversation.bedrock_message_history().len());
let drained: Vec<_> = conversation
.bedrock_message_history()
.iter()
.take(drain_count)
.cloned()
.collect();
conversation.archive_tool_results(drained);
conversation
.bedrock_message_history_mut()
.drain(0..drain_count);
conversation
.set_progressive_summary(Some(summary_text.clone()), drain_count);
conversation.set_has_pending_progressive_summary(false);
// Estimate new context window usage
let summary_tokens = (summary_text.len() / 4) as u32;
let remaining_msgs_tokens: u32 = conversation
.bedrock_message_history()
.iter()
.map(|m| match &m.content {
MessageContent::Text(t) => (t.len() / 4) as u32,
MessageContent::ToolUse { input, .. } => {
(input.to_string().len() / 4) as u32 + 20
}
MessageContent::ToolResult { content, .. } => {
(content.len() / 4) as u32
}
MessageContent::MultiPart(parts) => {
use crate::ai::bedrock::convert::ContentPart;
parts
.iter()
.map(|p| match p {
ContentPart::Text(t) => (t.len() / 4) as u32,
ContentPart::ToolUse { input, .. } => {
(input.to_string().len() / 4) as u32
}
ContentPart::ToolResult { content, .. } => {
(content.len() / 4) as u32
}
})
.sum()
}
})
.sum();
let max_ctx = context_window_for_model(&active_model_id);
let new_usage =
(summary_tokens + remaining_msgs_tokens) as f32 / max_ctx as f32;
conversation.set_context_window_usage(new_usage);
conversation
.set_current_context_tokens(summary_tokens + remaining_msgs_tokens);
log::info!(
"[progressive-summary] Post-summary: ~{} tokens ({:.1}% of {} context), {} messages retained",
summary_tokens + remaining_msgs_tokens,
new_usage * 100.0,
active_model_id,
conversation.bedrock_message_history().len()
);
}
});
// Update cost tracking
history_model.update(ctx, |history_model, _| {
use warp_multi_agent_api::response_event::stream_finished;
let token_usage = vec![stream_finished::TokenUsage {
model_id: "bedrock".to_string(),
total_input: input_tokens,
output: output_tokens,
input_cache_read: 0,
input_cache_write: 0,
cost_in_cents: cost_cents,
}];
history_model.update_conversation_cost_and_usage_for_request(
conversation_id,
None,
token_usage,
None,
false,
);
});
}
Err(e) => {
log::error!(
"[progressive-summary] Failed for {:?}: {:?}",
conversation_id,
e
);
history_model.update(ctx, |history_model, _| {
if let Some(conversation) =
history_model.conversation_mut(&conversation_id)
{
conversation.set_has_pending_progressive_summary(false);
}
});
}
}
let _ = me;
},
);
}
}
impl Entity for BlocklistAIController {
@@ -3028,7 +3412,9 @@ fn is_bedrock_credentials_error(msg: &str) -> bool {
|| (lower.contains("sso/cache") && lower.contains("notfound"))
|| (lower.contains("sso/cache") && lower.contains("no such file"))
|| (lower.contains("accessdenied")
&& (lower.contains("token") || lower.contains("credential") || lower.contains("security")))
&& (lower.contains("token")
|| lower.contains("credential")
|| lower.contains("security")))
}
#[allow(clippy::too_many_arguments)]