Skip to content

Commit 01ea34c

Browse files
committed
Auto compaction
1 parent 8e92f1b commit 01ea34c

File tree

2 files changed

+107
-4
lines changed

2 files changed

+107
-4
lines changed

crates/but-claude/src/bridge.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ impl Claudes {
7777
if self.requests.lock().await.contains_key(&stack_id) {
7878
bail!(
7979
"Claude is currently thinking, please wait for it to complete before sending another message.\n\nIf claude is stuck thinking, try restarting the application."
80-
)
80+
);
8181
} else {
82-
self.spawn_claude(ctx, broadcaster, stack_id, user_params)
83-
.await
82+
self.spawn_claude(ctx.clone(), broadcaster.clone(), stack_id, user_params)
83+
.await;
84+
let _ = self.maybe_compact_context(ctx, broadcaster, stack_id).await;
8485
};
8586

8687
Ok(())

crates/but-claude/src/compact.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
//! of the conversation so far and then start a new session where the first
1313
//! message contains the summary
1414
15-
use std::sync::Arc;
15+
use std::{collections::HashMap, sync::Arc};
1616

1717
use anyhow::{Context, Result};
1818
use but_broadcaster::Broadcaster;
1919
use but_workspace::StackId;
2020
use gitbutler_command_context::CommandContext;
2121
use gix::bstr::ByteSlice;
22+
use serde::Deserialize;
2223
use tokio::{
2324
process::Command,
2425
sync::{Mutex, mpsc::unbounded_channel},
@@ -32,6 +33,47 @@ use crate::{
3233
send_claude_message,
3334
};
3435

36+
#[derive(Deserialize, Debug, Clone)]
37+
#[serde(rename_all = "camelCase")]
38+
struct ModelUsage {
39+
input_tokens: u32,
40+
output_tokens: u32,
41+
cache_read_input_tokens: Option<u32>,
42+
}
43+
44+
#[derive(Debug)]
45+
struct Model<'a> {
46+
name: &'a str,
47+
subtype: Option<&'a str>,
48+
context: u32,
49+
}
50+
51+
const COMPACTION_BUFFER: u32 = 15_000;
52+
53+
const MODELS: &[Model<'static>] = &[
54+
Model {
55+
name: "opus",
56+
subtype: None,
57+
context: 200_000,
58+
},
59+
// Ordering the 1m model before the 200k model so it matches first.
60+
Model {
61+
name: "sonnet",
62+
subtype: Some("[1m]"),
63+
context: 1_000_000,
64+
},
65+
Model {
66+
name: "sonnet",
67+
subtype: None,
68+
context: 200_000,
69+
},
70+
Model {
71+
name: "haiku",
72+
subtype: None,
73+
context: 200_000,
74+
},
75+
];
76+
3577
impl Claudes {
3678
pub(crate) async fn compact(
3779
&self,
@@ -122,6 +164,66 @@ impl Claudes {
122164

123165
Ok(())
124166
}
167+
168+
pub(crate) async fn maybe_compact_context(
169+
&self,
170+
ctx: Arc<Mutex<CommandContext>>,
171+
broadcaster: Arc<tokio::sync::Mutex<Broadcaster>>,
172+
stack_id: StackId,
173+
) -> Result<()> {
174+
let rule = {
175+
let mut ctx = ctx.lock().await;
176+
list_claude_assignment_rules(&mut ctx)?
177+
.into_iter()
178+
.find(|rule| rule.stack_id == stack_id)
179+
};
180+
let Some(rule) = rule else {
181+
return Ok(());
182+
};
183+
184+
let messages = {
185+
let mut ctx = ctx.lock().await;
186+
db::list_messages_by_session(&mut ctx, rule.session_id)?
187+
};
188+
189+
// Find the last result message
190+
let Some(output) = messages.into_iter().rev().find_map(|m| match m.content {
191+
ClaudeMessageContent::ClaudeOutput(o) => {
192+
if o["type"].as_str() == Some("result") {
193+
Some(o)
194+
} else {
195+
None
196+
}
197+
}
198+
_ => None,
199+
}) else {
200+
return Ok(());
201+
};
202+
203+
let usage: HashMap<String, ModelUsage> =
204+
serde_json::from_value(output["modelUsage"].clone())?;
205+
206+
for (name, usage) in usage {
207+
if let Some(model) = find_model(name) {
208+
let total = usage.cache_read_input_tokens.unwrap_or(0)
209+
+ usage.input_tokens
210+
+ usage.output_tokens;
211+
if total > (model.context - COMPACTION_BUFFER) {
212+
self.compact(ctx.clone(), broadcaster.clone(), stack_id)
213+
.await;
214+
break;
215+
}
216+
};
217+
}
218+
219+
Ok(())
220+
}
221+
}
222+
223+
fn find_model(name: String) -> Option<&'static Model<'static>> {
224+
MODELS
225+
.iter()
226+
.find(|&m| name.contains(m.name) && m.subtype.map(|s| name.contains(s)).unwrap_or(true))
125227
}
126228

127229
pub async fn generate_summary(

0 commit comments

Comments
 (0)