cargo fmt, allow rate-limiting requests to claude
This commit is contained in:
@@ -267,9 +267,7 @@ impl AppCode {
|
|||||||
/// If `path` is None or empty, lists the root directory.
|
/// If `path` is None or empty, lists the root directory.
|
||||||
pub async fn ls(&self, path: Option<&str>) -> Result<String, String> {
|
pub async fn ls(&self, path: Option<&str>) -> Result<String, String> {
|
||||||
let cache = self.get_current_tarball().await;
|
let cache = self.get_current_tarball().await;
|
||||||
let cached = cache
|
let cached = cache.as_ref().ok_or("source code not available")?;
|
||||||
.as_ref()
|
|
||||||
.ok_or("source code not available")?;
|
|
||||||
|
|
||||||
let prefix = path.unwrap_or("").trim_start_matches('/');
|
let prefix = path.unwrap_or("").trim_start_matches('/');
|
||||||
let prefix = if prefix.is_empty() {
|
let prefix = if prefix.is_empty() {
|
||||||
@@ -293,16 +291,17 @@ impl AppCode {
|
|||||||
|
|
||||||
// Get just the first component (file or directory name)
|
// Get just the first component (file or directory name)
|
||||||
if let Some(first) = remainder.split('/').next()
|
if let Some(first) = remainder.split('/').next()
|
||||||
&& !first.is_empty() {
|
&& !first.is_empty()
|
||||||
// Check if it's a directory (has more components)
|
{
|
||||||
let is_dir = remainder.contains('/');
|
// Check if it's a directory (has more components)
|
||||||
let entry = if is_dir {
|
let is_dir = remainder.contains('/');
|
||||||
format!("{}/", first)
|
let entry = if is_dir {
|
||||||
} else {
|
format!("{}/", first)
|
||||||
first.to_string()
|
} else {
|
||||||
};
|
first.to_string()
|
||||||
entries.insert(entry);
|
};
|
||||||
}
|
entries.insert(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,9 +320,7 @@ impl AppCode {
|
|||||||
/// Supports simple glob patterns with `*` wildcards.
|
/// Supports simple glob patterns with `*` wildcards.
|
||||||
pub async fn find(&self, pattern: Option<&str>) -> Result<String, String> {
|
pub async fn find(&self, pattern: Option<&str>) -> Result<String, String> {
|
||||||
let cache = self.get_current_tarball().await;
|
let cache = self.get_current_tarball().await;
|
||||||
let cached = cache
|
let cached = cache.as_ref().ok_or("source code not available")?;
|
||||||
.as_ref()
|
|
||||||
.ok_or("source code not available")?;
|
|
||||||
|
|
||||||
let pattern = pattern.unwrap_or("*");
|
let pattern = pattern.unwrap_or("*");
|
||||||
|
|
||||||
@@ -357,9 +354,7 @@ impl AppCode {
|
|||||||
line_end: Option<usize>,
|
line_end: Option<usize>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let cache = self.get_current_tarball().await;
|
let cache = self.get_current_tarball().await;
|
||||||
let cached = cache
|
let cached = cache.as_ref().ok_or("source code not available")?;
|
||||||
.as_ref()
|
|
||||||
.ok_or("source code not available")?;
|
|
||||||
|
|
||||||
let path = path.trim_start_matches('/');
|
let path = path.trim_start_matches('/');
|
||||||
let file = cached
|
let file = cached
|
||||||
@@ -404,9 +399,7 @@ impl AppCode {
|
|||||||
let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))?;
|
let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex: {e}"))?;
|
||||||
|
|
||||||
let cache = self.get_current_tarball().await;
|
let cache = self.get_current_tarball().await;
|
||||||
let cached = cache
|
let cached = cache.as_ref().ok_or("source code not available")?;
|
||||||
.as_ref()
|
|
||||||
.ok_or("source code not available")?;
|
|
||||||
|
|
||||||
let prefix = path_prefix.map(|p| p.trim_start_matches('/'));
|
let prefix = path_prefix.map(|p| p.trim_start_matches('/'));
|
||||||
|
|
||||||
@@ -421,9 +414,10 @@ impl AppCode {
|
|||||||
'outer: for (path, file) in sorted_files {
|
'outer: for (path, file) in sorted_files {
|
||||||
// Skip if path doesn't match prefix
|
// Skip if path doesn't match prefix
|
||||||
if let Some(prefix) = prefix
|
if let Some(prefix) = prefix
|
||||||
&& !path.starts_with(prefix) {
|
&& !path.starts_with(prefix)
|
||||||
continue;
|
{
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip binary-looking files
|
// Skip binary-looking files
|
||||||
if looks_binary(&file.content) {
|
if looks_binary(&file.content) {
|
||||||
@@ -472,10 +466,11 @@ impl AppCode {
|
|||||||
|
|
||||||
// Add separator if there's a gap
|
// Add separator if there's a gap
|
||||||
if let Some(&last) = printed.iter().next_back()
|
if let Some(&last) = printed.iter().next_back()
|
||||||
&& start > last + 1 {
|
&& start > last + 1
|
||||||
writeln!(&mut output, "---")
|
{
|
||||||
.map_err(|e| format!("Format error: {e}"))?;
|
writeln!(&mut output, "---")
|
||||||
}
|
.map_err(|e| format!("Format error: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
for (i, line) in lines[start..end].iter().enumerate() {
|
for (i, line) in lines[start..end].iter().enumerate() {
|
||||||
let line_idx = start + i;
|
let line_idx = start + i;
|
||||||
@@ -662,8 +657,7 @@ impl ToolExecutor for AppCodeTools {
|
|||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "code_search",
|
name: "code_search",
|
||||||
description:
|
description: "Search for a regex pattern in an application's source code (like grep).",
|
||||||
"Search for a regex pattern in an application's source code (like grep).",
|
|
||||||
input_schema: serde_json::json!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -26,21 +26,20 @@ impl AppCodeConfigExt {
|
|||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let response = client
|
let response = client.get(url.as_str()).send().await.map_err(
|
||||||
.get(url.as_str())
|
|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
|
||||||
Box::new(std::io::Error::other(format!(
|
Box::new(std::io::Error::other(format!(
|
||||||
"HTTP request to {url} failed: {e}"
|
"HTTP request to {url} failed: {e}"
|
||||||
)))
|
)))
|
||||||
})?;
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(Box::new(std::io::Error::other(format!(
|
return Err(Box::new(std::io::Error::other(format!(
|
||||||
"HTTP request to {url} returned {}",
|
"HTTP request to {url} returned {}",
|
||||||
response.status()
|
response.status()
|
||||||
))) as Box<dyn std::error::Error + Send + Sync>);
|
)))
|
||||||
|
as Box<dyn std::error::Error + Send + Sync>);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sha = response
|
let mut sha = response
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ pub use worker::SentBy;
|
|||||||
use crate::message_handler::AdminMessageResponse;
|
use crate::message_handler::AdminMessageResponse;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use conf::Conf;
|
use conf::Conf;
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Weak, time::Duration};
|
||||||
use std::sync::Weak;
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use worker::{ChatMessage, ClaudeWorker, Input};
|
use worker::{ChatMessage, ClaudeWorker, Input};
|
||||||
|
|
||||||
@@ -69,6 +68,11 @@ pub struct CompactionConfig {
|
|||||||
/// Trigger compaction when message buffer exceeds this many characters.
|
/// Trigger compaction when message buffer exceeds this many characters.
|
||||||
#[conf(long, env, default_value = "50000")]
|
#[conf(long, env, default_value = "50000")]
|
||||||
pub trigger_chars: u32,
|
pub trigger_chars: u32,
|
||||||
|
/// Minimum interval between automatic compactions (not user-requested).
|
||||||
|
/// If omitted, AI compaction always runs (no rate limiting).
|
||||||
|
/// If set, compaction is rate-limited and low-priority messages are dropped instead.
|
||||||
|
#[conf(long, env, value_parser = conf_extra::parse_duration, serde(use_value_parser))]
|
||||||
|
pub min_interval: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error type for Claude API operations.
|
/// Error type for Claude API operations.
|
||||||
@@ -172,7 +176,7 @@ impl ClaudeAgent {
|
|||||||
result_rx.await.map_err(|_| ClaudeError::WorkerGone)?
|
result_rx.await.map_err(|_| ClaudeError::WorkerGone)?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record a message into claude's chat log that claude is not expected to respond to
|
/// Record a message into claude's chat log that claude is not expected to respond to.
|
||||||
pub fn record_message(&self, sent_by: SentBy, text: &str, ts_ms: u64) {
|
pub fn record_message(&self, sent_by: SentBy, text: &str, ts_ms: u64) {
|
||||||
let timestamp = DateTime::from_timestamp_millis(ts_ms as i64).unwrap_or_else(Utc::now);
|
let timestamp = DateTime::from_timestamp_millis(ts_ms as i64).unwrap_or_else(Utc::now);
|
||||||
let msg = ChatMessage {
|
let msg = ChatMessage {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ use crate::message_handler::AdminMessageResponse;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Weak};
|
||||||
use std::sync::Weak;
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
@@ -94,6 +93,8 @@ pub struct ClaudeWorker {
|
|||||||
tool_executor: Weak<dyn ToolExecutor>,
|
tool_executor: Weak<dyn ToolExecutor>,
|
||||||
input_rx: mpsc::Receiver<Input>,
|
input_rx: mpsc::Receiver<Input>,
|
||||||
stop_rx: mpsc::Receiver<()>,
|
stop_rx: mpsc::Receiver<()>,
|
||||||
|
/// Timestamp of the last automatic compaction (not user-requested).
|
||||||
|
last_auto_compaction: Option<std::time::Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClaudeWorker {
|
impl ClaudeWorker {
|
||||||
@@ -137,6 +138,7 @@ impl ClaudeWorker {
|
|||||||
tool_executor,
|
tool_executor,
|
||||||
input_rx,
|
input_rx,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
|
last_auto_compaction: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,10 +156,22 @@ impl ClaudeWorker {
|
|||||||
let (mc, maybe_sender) = msg.into_content_and_sender();
|
let (mc, maybe_sender) = msg.into_content_and_sender();
|
||||||
self.messages.push(mc);
|
self.messages.push(mc);
|
||||||
|
|
||||||
// Check if we need to trigger compaction
|
// Check if we need to trigger automatic compaction
|
||||||
let buffer_chars = self.message_buffer_chars();
|
let buffer_chars = self.message_buffer_chars();
|
||||||
if buffer_chars > self.config.compaction.trigger_chars as usize {
|
if buffer_chars > self.config.compaction.trigger_chars as usize {
|
||||||
self.handle_compact().await;
|
// Check if rate limiting is configured
|
||||||
|
let can_compact = match self.config.compaction.min_interval {
|
||||||
|
None => true, // No rate limit configured
|
||||||
|
Some(min_interval) => self.last_auto_compaction
|
||||||
|
.map(|t| t.elapsed() >= min_interval)
|
||||||
|
.unwrap_or(true),
|
||||||
|
};
|
||||||
|
if can_compact {
|
||||||
|
self.handle_compact(true).await;
|
||||||
|
} else {
|
||||||
|
// Rate limited - drop oldest messages instead
|
||||||
|
self.drop_oldest_messages();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sender) = maybe_sender {
|
if let Some(sender) = maybe_sender {
|
||||||
@@ -171,7 +185,7 @@ impl ClaudeWorker {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Input::Compact => {
|
Input::Compact => {
|
||||||
self.handle_compact().await;
|
self.handle_compact(false).await;
|
||||||
}
|
}
|
||||||
Input::Debug => {
|
Input::Debug => {
|
||||||
self.handle_debug();
|
self.handle_debug();
|
||||||
@@ -202,6 +216,43 @@ impl ClaudeWorker {
|
|||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop oldest messages until buffer is under the trigger threshold.
|
||||||
|
fn drop_oldest_messages(&mut self) {
|
||||||
|
let target = self.config.compaction.trigger_chars as usize;
|
||||||
|
let before_chars = self.message_buffer_chars();
|
||||||
|
|
||||||
|
if before_chars <= target {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_chars = before_chars;
|
||||||
|
let mut dropped = 0;
|
||||||
|
|
||||||
|
// Remove from the front (oldest) until under limit
|
||||||
|
while current_chars > target && !self.messages.is_empty() {
|
||||||
|
let msg_chars = self.messages[0]
|
||||||
|
.content
|
||||||
|
.iter()
|
||||||
|
.map(|block| match block {
|
||||||
|
ContentBlock::Text { text } => text.len(),
|
||||||
|
ContentBlock::ToolUse { input, .. } => estimate_json_size(input),
|
||||||
|
ContentBlock::ToolResult { content, .. } => content.len(),
|
||||||
|
})
|
||||||
|
.sum::<usize>();
|
||||||
|
current_chars = current_chars.saturating_sub(msg_chars);
|
||||||
|
self.messages.remove(0);
|
||||||
|
dropped += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let after_chars = self.message_buffer_chars();
|
||||||
|
warn!(
|
||||||
|
"Compaction rate-limited: dropped {} oldest messages ({} -> {} chars)",
|
||||||
|
dropped,
|
||||||
|
before_chars,
|
||||||
|
after_chars
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a stop request by draining queues and sending errors to pending requests.
|
/// Handle a stop request by draining queues and sending errors to pending requests.
|
||||||
fn handle_stop(&mut self) {
|
fn handle_stop(&mut self) {
|
||||||
// Drain the stop_rx queue
|
// Drain the stop_rx queue
|
||||||
@@ -232,16 +283,26 @@ impl ClaudeWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Perform compaction by summarizing messages and storing the result.
|
/// Perform compaction by summarizing messages and storing the result.
|
||||||
async fn handle_compact(&mut self) {
|
///
|
||||||
|
/// If `is_automatic` is true, this was triggered by buffer size threshold
|
||||||
|
/// and the last_auto_compaction timestamp will be updated.
|
||||||
|
async fn handle_compact(&mut self, is_automatic: bool) {
|
||||||
if self.messages.is_empty() {
|
if self.messages.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update timestamp for automatic compactions (before the API call)
|
||||||
|
if is_automatic {
|
||||||
|
self.last_auto_compaction = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
let num_messages = self.messages.len();
|
let num_messages = self.messages.len();
|
||||||
let buffer_chars = self.message_buffer_chars();
|
let buffer_chars = self.message_buffer_chars();
|
||||||
warn!(
|
warn!(
|
||||||
"Starting compaction: {} messages, {} chars",
|
"Starting {} compaction: {} messages, {} chars",
|
||||||
num_messages, buffer_chars
|
if is_automatic { "automatic" } else { "manual" },
|
||||||
|
num_messages,
|
||||||
|
buffer_chars
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build system content: compaction prompt first, then other system prompts, then existing summary
|
// Build system content: compaction prompt first, then other system prompts, then existing summary
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ use http_body::Body;
|
|||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use prometheus_http_client::{AlertStatus, ExtractLabels};
|
use prometheus_http_client::{AlertStatus, ExtractLabels};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write, net::SocketAddr, path::PathBuf, sync::Arc, sync::Mutex, sync::OnceLock, sync::Weak,
|
fmt::Write,
|
||||||
|
net::SocketAddr,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex, OnceLock, Weak},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
//! - Sequence: `["uuid1", "uuid2"]` - UUIDs with no safety numbers (simpler)
|
//! - Sequence: `["uuid1", "uuid2"]` - UUIDs with no safety numbers (simpler)
|
||||||
|
|
||||||
use crate::signal_jsonrpc::{Envelope, Identity, RpcClient};
|
use crate::signal_jsonrpc::{Envelope, Identity, RpcClient};
|
||||||
use serde::de::{MapAccess, SeqAccess, Visitor};
|
use serde::{
|
||||||
use serde::{Deserialize, Deserializer};
|
Deserialize, Deserializer,
|
||||||
use std::borrow::Borrow;
|
de::{MapAccess, SeqAccess, Visitor},
|
||||||
use std::collections::HashMap;
|
};
|
||||||
use std::fmt;
|
use std::{borrow::Borrow, collections::HashMap, fmt, ops::Deref, str::FromStr};
|
||||||
use std::ops::Deref;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
/// A validated Signal UUID in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
/// A validated Signal UUID in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ use prometheus_http_client::{
|
|||||||
AlertInfo, AlertsRequest, ExtractLabels, Labels, LabelsRequest, MetricVal, MetricValue,
|
AlertInfo, AlertsRequest, ExtractLabels, Labels, LabelsRequest, MetricVal, MetricValue,
|
||||||
PromRequest, QueryRequest, ReqwestClient, SeriesRequest,
|
PromRequest, QueryRequest, ReqwestClient, SeriesRequest,
|
||||||
};
|
};
|
||||||
use std::error::Error;
|
use std::{error::Error, fmt::Write};
|
||||||
use std::fmt::Write;
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
type BoxError = Box<dyn Error + Send + Sync>;
|
type BoxError = Box<dyn Error + Send + Sync>;
|
||||||
|
|||||||
Reference in New Issue
Block a user