huskies: merge 1186 story compact chat command: distill session context deterministically, then reset with a seed
This commit is contained in:
@@ -411,6 +411,7 @@ async fn handle_llm_message(ctx: &DiscordContext, channel: &str, user: &str, use
|
||||
Ok(ClaudeCodeResult {
|
||||
messages,
|
||||
session_id,
|
||||
..
|
||||
}) => {
|
||||
let reply = if !remaining.is_empty() {
|
||||
let _ = msg_tx.send(remaining.clone());
|
||||
@@ -568,6 +569,7 @@ mod tests {
|
||||
sender: "user123".to_string(),
|
||||
content: "previous message".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
m
|
||||
|
||||
@@ -56,6 +56,7 @@ mod tests {
|
||||
content: "hi there!".to_string(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -120,6 +120,16 @@ pub struct BotContext {
|
||||
/// Optional model override from bot.toml. Passed as `--model` to the
|
||||
/// `claude` CLI when set.
|
||||
pub model: Option<String>,
|
||||
/// Maximum size in bytes of the digest the `compact` command writes as a
|
||||
/// seed file. From `bot.toml`'s `compact_seed_max_bytes`.
|
||||
pub compact_seed_max_bytes: usize,
|
||||
/// `cache_read_input_tokens` threshold above which the bot suggests
|
||||
/// running `compact` after a turn. From `bot.toml`'s
|
||||
/// `cache_read_suggest_threshold`.
|
||||
pub cache_read_suggest_threshold: u64,
|
||||
/// Minimum seconds between repeated `compact` suggestions for the same
|
||||
/// room. From `bot.toml`'s `compact_suggest_cooldown_secs`.
|
||||
pub compact_suggest_cooldown_secs: i64,
|
||||
}
|
||||
|
||||
impl BotContext {
|
||||
@@ -343,6 +353,9 @@ mod tests {
|
||||
gateway_port: None,
|
||||
last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
|
||||
model: None,
|
||||
compact_seed_max_bytes: 8_000,
|
||||
cache_read_suggest_threshold: 50_000,
|
||||
compact_suggest_cooldown_secs: 3_600,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,17 @@ pub struct RoomConversation {
|
||||
pub session_id: Option<String>,
|
||||
/// Rolling conversation entries (used for turn counting and persistence).
|
||||
pub entries: Vec<ConversationEntry>,
|
||||
/// A distilled digest produced by the `compact` command, waiting to be
|
||||
/// injected as background context into the next spawned session's
|
||||
/// prompt. Cleared immediately after the first turn that uses it, so it
|
||||
/// is never re-injected.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pending_seed: Option<String>,
|
||||
/// Timestamp (ms since Unix epoch) of the last time this room was sent a
|
||||
/// "consider running `compact`" suggestion, used to rate-limit repeat
|
||||
/// suggestions.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_compact_suggested_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
/// Per-room conversation state, keyed by room ID (serialised as string).
|
||||
|
||||
@@ -31,6 +31,23 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
guard.get(&room_id).and_then(|conv| conv.session_id.clone())
|
||||
};
|
||||
|
||||
// Drain any pending `compact` seed for this room so it is injected into
|
||||
// the prompt exactly once, then persist the cleared state immediately —
|
||||
// a crash mid-turn must not cause it to be re-injected on the next try.
|
||||
let pending_seed: Option<String> = {
|
||||
let mut guard = ctx.history.lock().await;
|
||||
let conv = guard.entry(room_id.clone()).or_default();
|
||||
let seed = conv.pending_seed.take();
|
||||
if seed.is_some() {
|
||||
save_history(&ctx.services.project_root, &guard);
|
||||
}
|
||||
seed
|
||||
};
|
||||
let seed_prefix = pending_seed
|
||||
.as_deref()
|
||||
.map(crate::chat::compact::frame_seed_for_prompt)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Pull new pipeline-transition events from the CRDT event log for this
|
||||
// persona and atomically advance the high-water marks so the same events
|
||||
// are not re-injected on the next turn. All transports share the same
|
||||
@@ -49,7 +66,7 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
String::new()
|
||||
};
|
||||
let prompt = format!(
|
||||
"{event_log_ctx}[Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude.]\n{active_project_ctx}\n{}",
|
||||
"{event_log_ctx}{seed_prefix}[Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude.]\n{active_project_ctx}\n{}",
|
||||
format_user_prompt(&sender, &user_message)
|
||||
);
|
||||
|
||||
@@ -127,10 +144,11 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
let remaining = buffer.lock().unwrap().trim().to_string();
|
||||
let did_send_any = sent_any_chunk.load(Ordering::Relaxed);
|
||||
|
||||
let (assistant_reply, new_session_id) = match result {
|
||||
let (assistant_reply, new_session_id, turn_usage) = match result {
|
||||
Ok(ClaudeCodeResult {
|
||||
messages,
|
||||
session_id,
|
||||
usage,
|
||||
}) => {
|
||||
let reply = if !remaining.is_empty() {
|
||||
let _ = msg_tx.send(remaining.clone());
|
||||
@@ -153,7 +171,7 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
remaining
|
||||
};
|
||||
slog!("[matrix-bot] session_id from chat_stream: {:?}", session_id);
|
||||
(reply, session_id)
|
||||
(reply, session_id, usage)
|
||||
}
|
||||
Err(e) => {
|
||||
slog!("[matrix-bot] LLM error: {e}");
|
||||
@@ -163,7 +181,7 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
format!("Error processing your request: {e}")
|
||||
};
|
||||
let _ = msg_tx.send(err_msg.clone());
|
||||
(err_msg, None)
|
||||
(err_msg, None, None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,9 +192,10 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
|
||||
// Record this exchange in the per-room conversation history and persist
|
||||
// the session ID so the next turn resumes with structured API messages.
|
||||
let mut compact_suggestion: Option<String> = None;
|
||||
if !assistant_reply.starts_with("Error processing") {
|
||||
let mut guard = ctx.history.lock().await;
|
||||
let conv = guard.entry(room_id).or_default();
|
||||
let conv = guard.entry(room_id.clone()).or_default();
|
||||
|
||||
// Store the session ID so the next turn uses --resume.
|
||||
slog!(
|
||||
@@ -208,6 +227,27 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
conv.entries.drain(..excess);
|
||||
}
|
||||
|
||||
// When this turn's cache_read usage crosses the configured threshold,
|
||||
// queue a rate-limited suggestion to run `compact`. Rate-limited via
|
||||
// `last_compact_suggested_at_ms` so a busy room isn't nagged every turn.
|
||||
if let Some(usage) = &turn_usage
|
||||
&& usage.cache_read_input_tokens > ctx.cache_read_suggest_threshold
|
||||
{
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
let cooldown_ms = ctx.compact_suggest_cooldown_secs.saturating_mul(1_000);
|
||||
let due = conv
|
||||
.last_compact_suggested_at_ms
|
||||
.is_none_or(|last| now_ms - last >= cooldown_ms);
|
||||
if due {
|
||||
conv.last_compact_suggested_at_ms = Some(now_ms);
|
||||
compact_suggestion = Some(format!(
|
||||
"This turn read {} cache tokens. Consider running `compact` to distill the \
|
||||
session and reduce context size.",
|
||||
usage.cache_read_input_tokens
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Persist to disk so history survives server restarts.
|
||||
save_history(&ctx.services.project_root, &guard);
|
||||
} else {
|
||||
@@ -222,6 +262,14 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message(
|
||||
save_history(&ctx.services.project_root, &guard);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(suggestion) = compact_suggestion {
|
||||
let html = markdown_to_html(&suggestion);
|
||||
let _ = ctx
|
||||
.transport
|
||||
.send_message(&room_id_str, &suggestion, &html)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1023,6 +1023,36 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for the compact command, which requires async access to the
|
||||
// shared conversation history and cannot be handled by the sync command
|
||||
// registry.
|
||||
if super::super::super::compact::extract_compact_command(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
slog!("[matrix-bot] Handling compact command from {sender}");
|
||||
let response = super::super::super::compact::handle_compact(
|
||||
&incoming_room_id,
|
||||
&ctx.history,
|
||||
&ctx.services.project_root,
|
||||
ctx.compact_seed_max_bytes,
|
||||
)
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
.send_message(&room_id_str, &response, &html)
|
||||
.await
|
||||
&& let Ok(event_id) = msg_id.parse()
|
||||
{
|
||||
ctx.bot_sent_event_ids.lock().await.insert(event_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In gateway mode, intercept "rebuild gateway" and route it through the
|
||||
// detached trampoline so the process swap survives any bash-tool kill cascade.
|
||||
if ctx.gateway_active_project.is_some()
|
||||
|
||||
@@ -334,6 +334,9 @@ pub async fn run_bot(
|
||||
gateway_port,
|
||||
last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
|
||||
model: config.model.clone(),
|
||||
compact_seed_max_bytes: config.compact_seed_max_bytes,
|
||||
cache_read_suggest_threshold: config.cache_read_suggest_threshold,
|
||||
compact_suggest_cooldown_secs: config.compact_suggest_cooldown_secs,
|
||||
};
|
||||
|
||||
slog!(
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
//! `compact` command: distills the current room's Claude Code session
|
||||
//! transcript into a size-capped digest, writes it as a seed, and clears the
|
||||
//! session so the next turn starts fresh with only the distilled context.
|
||||
//!
|
||||
//! Parsing and the transcript→digest→seed pipeline are transport-agnostic
|
||||
//! and live in [`crate::chat::compact`]; this module is the Matrix-specific
|
||||
//! glue around the shared [`ConversationHistory`], mirroring [`super::reset`].
|
||||
|
||||
use crate::chat::compact::{self, CompactError};
|
||||
use crate::chat::transport::matrix::bot::{ConversationHistory, RoomConversation};
|
||||
use matrix_sdk::ruma::OwnedRoomId;
|
||||
use std::path::Path;
|
||||
|
||||
/// Re-exported so Matrix call sites read as `compact::extract_compact_command`,
|
||||
/// matching the sibling `reset` module's shape.
|
||||
pub use crate::chat::compact::extract_compact_command;
|
||||
|
||||
/// Handle a `compact` command: distill the room's session transcript into a
|
||||
/// digest, write it as a seed for the next turn, clear the session ID and
|
||||
/// entries (like `reset`), and return a confirmation with before/after sizes.
|
||||
///
|
||||
/// `project_root` doubles as the working directory the `claude` CLI was
|
||||
/// spawned with for this room (matrix's `handle_message` always runs Claude
|
||||
/// Code in `services.project_root` — the gateway config dir in gateway mode,
|
||||
/// or the project root in standalone mode — so the session transcript lives
|
||||
/// under the matching `~/.claude/projects/<mangled project_root>/` directory).
|
||||
///
|
||||
/// Returns an actionable message and changes nothing when there is no active
|
||||
/// session, or when the session transcript cannot be read.
|
||||
pub async fn handle_compact(
|
||||
room_id: &OwnedRoomId,
|
||||
history: &ConversationHistory,
|
||||
project_root: &Path,
|
||||
max_bytes: usize,
|
||||
) -> String {
|
||||
let mut guard = history.lock().await;
|
||||
let conv = guard
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(RoomConversation::default);
|
||||
|
||||
let Some(session_id) = conv.session_id.clone() else {
|
||||
return "No active session to compact. Send a message first to start one.".to_string();
|
||||
};
|
||||
|
||||
let transcript_path = compact::transcript::transcript_path(project_root, &session_id);
|
||||
|
||||
match compact::compact_session(project_root, &transcript_path, room_id.as_ref(), max_bytes) {
|
||||
Ok(outcome) => {
|
||||
conv.session_id = None;
|
||||
conv.entries.clear();
|
||||
conv.pending_seed = Some(outcome.digest);
|
||||
crate::chat::transport::matrix::bot::save_history(project_root, &guard);
|
||||
crate::slog!(
|
||||
"[matrix-bot] compact: room {room_id} {} -> {} bytes, seed written to {}",
|
||||
outcome.before_bytes,
|
||||
outcome.after_bytes,
|
||||
outcome.seed_path.display()
|
||||
);
|
||||
format!(
|
||||
"Compacted session context: {} → {} bytes. Starting fresh — the distilled \
|
||||
summary will be included as background in your next message.",
|
||||
outcome.before_bytes, outcome.after_bytes
|
||||
)
|
||||
}
|
||||
Err(CompactError::TranscriptUnreadable(e)) => format!(
|
||||
"Could not compact: session transcript is unreadable ({e}). Nothing was changed."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::transport::matrix::bot::{ConversationEntry, ConversationRole};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
|
||||
fn room_id() -> OwnedRoomId {
|
||||
"!test:example.com".parse().unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_active_session_returns_actionable_message_and_changes_nothing() {
|
||||
let history: ConversationHistory = Arc::new(TokioMutex::new(HashMap::new()));
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
let response = handle_compact(&room_id(), &history, tmp.path(), 8_000).await;
|
||||
|
||||
assert!(
|
||||
response.contains("No active session"),
|
||||
"expected actionable no-session message: {response}"
|
||||
);
|
||||
let guard = history.lock().await;
|
||||
assert!(
|
||||
guard.get(&room_id()).is_none_or(|c| c.entries.is_empty()),
|
||||
"no entries should be created for a compact with no session"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unreadable_transcript_returns_actionable_message_and_preserves_session() {
|
||||
let history: ConversationHistory = Arc::new(TokioMutex::new({
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
room_id(),
|
||||
RoomConversation {
|
||||
session_id: Some("missing-session".to_string()),
|
||||
entries: vec![],
|
||||
pending_seed: None,
|
||||
last_compact_suggested_at_ms: None,
|
||||
},
|
||||
);
|
||||
m
|
||||
}));
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
let response = handle_compact(&room_id(), &history, tmp.path(), 8_000).await;
|
||||
|
||||
assert!(
|
||||
response.contains("unreadable"),
|
||||
"expected actionable unreadable-transcript message: {response}"
|
||||
);
|
||||
let guard = history.lock().await;
|
||||
let conv = guard.get(&room_id()).unwrap();
|
||||
assert_eq!(
|
||||
conv.session_id.as_deref(),
|
||||
Some("missing-session"),
|
||||
"session_id must be preserved when compaction fails"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn successful_compact_clears_session_and_sets_pending_seed() {
|
||||
let session_id = "sess-compact-1";
|
||||
let project_root = tempfile::tempdir().unwrap();
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
// SAFETY: this test owns HOME for its duration; no other test in this
|
||||
// process reads HOME concurrently with this call.
|
||||
unsafe {
|
||||
std::env::set_var("HOME", home.path());
|
||||
}
|
||||
let transcript_dir = compact::transcript::transcript_path(project_root.path(), session_id)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
std::fs::create_dir_all(&transcript_dir).unwrap();
|
||||
let jsonl = r#"{"type":"user","message":{"role":"user","content":"hello"}}
|
||||
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hi there"}]}}"#;
|
||||
std::fs::write(transcript_dir.join(format!("{session_id}.jsonl")), jsonl).unwrap();
|
||||
|
||||
let history: ConversationHistory = Arc::new(TokioMutex::new({
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
room_id(),
|
||||
RoomConversation {
|
||||
session_id: Some(session_id.to_string()),
|
||||
entries: vec![ConversationEntry {
|
||||
role: ConversationRole::User,
|
||||
sender: "@alice:example.com".to_string(),
|
||||
content: "hi".to_string(),
|
||||
}],
|
||||
pending_seed: None,
|
||||
last_compact_suggested_at_ms: None,
|
||||
},
|
||||
);
|
||||
m
|
||||
}));
|
||||
|
||||
let response = handle_compact(&room_id(), &history, project_root.path(), 8_000).await;
|
||||
|
||||
assert!(
|
||||
response.contains("Compacted session context"),
|
||||
"response should confirm compaction with sizes: {response}"
|
||||
);
|
||||
let guard = history.lock().await;
|
||||
let conv = guard.get(&room_id()).unwrap();
|
||||
assert!(conv.session_id.is_none(), "session_id must be cleared");
|
||||
assert!(conv.entries.is_empty(), "entries must be cleared");
|
||||
assert_eq!(
|
||||
conv.pending_seed.as_deref(),
|
||||
Some("User: hello\nAssistant: hi there"),
|
||||
"pending_seed must hold the distilled digest"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,23 @@ pub(super) fn default_coalesce_window_ms() -> u64 {
|
||||
1_500
|
||||
}
|
||||
|
||||
/// Default cap (bytes) on the digest the `compact` command writes as a seed.
|
||||
pub(super) fn default_compact_seed_max_bytes() -> usize {
|
||||
8_000
|
||||
}
|
||||
|
||||
/// Default cache_read token threshold above which the bot suggests running
|
||||
/// `compact` after a turn.
|
||||
pub(super) fn default_cache_read_suggest_threshold() -> u64 {
|
||||
50_000
|
||||
}
|
||||
|
||||
/// Default cooldown (seconds) between repeated `compact` suggestions for the
|
||||
/// same room.
|
||||
pub(super) fn default_compact_suggest_cooldown_secs() -> i64 {
|
||||
3_600
|
||||
}
|
||||
|
||||
pub(super) fn default_transport() -> String {
|
||||
"matrix".to_string()
|
||||
}
|
||||
@@ -190,4 +207,20 @@ pub struct BotConfig {
|
||||
/// `git config user.email` when absent.
|
||||
#[serde(default)]
|
||||
pub git_user_email: Option<String>,
|
||||
|
||||
/// Maximum size in bytes of the digest the `compact` command writes as a
|
||||
/// seed file. Older content is dropped first so the most recent
|
||||
/// conversation survives the cap. Defaults to 8000 bytes.
|
||||
#[serde(default = "default_compact_seed_max_bytes")]
|
||||
pub compact_seed_max_bytes: usize,
|
||||
|
||||
/// Number of `cache_read_input_tokens` in a single turn above which the
|
||||
/// bot suggests running `compact`. Defaults to 50 000.
|
||||
#[serde(default = "default_cache_read_suggest_threshold")]
|
||||
pub cache_read_suggest_threshold: u64,
|
||||
|
||||
/// Minimum seconds between repeated `compact` suggestions for the same
|
||||
/// room, so a busy room isn't spammed every turn. Defaults to 3600 (1h).
|
||||
#[serde(default = "default_compact_suggest_cooldown_secs")]
|
||||
pub compact_suggest_cooldown_secs: i64,
|
||||
}
|
||||
|
||||
@@ -925,6 +925,9 @@ mod tests {
|
||||
gateway_port: None,
|
||||
last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
|
||||
model: None,
|
||||
compact_seed_max_bytes: 8_000,
|
||||
cache_read_suggest_threshold: 50_000,
|
||||
compact_suggest_cooldown_secs: 3_600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ mod bot;
|
||||
pub mod cleanup_worktrees;
|
||||
/// Matrix bot command handlers — parses and routes bot commands from Matrix messages.
|
||||
pub mod commands;
|
||||
/// `compact` command — distills the session transcript into a seed and resets, Matrix-specific glue.
|
||||
pub mod compact;
|
||||
pub(crate) mod config;
|
||||
/// Story deletion command — handles `!delete` bot commands to remove work items.
|
||||
pub mod delete;
|
||||
|
||||
@@ -99,6 +99,9 @@ mod tests {
|
||||
gateway_port: None,
|
||||
last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
|
||||
model: None,
|
||||
compact_seed_max_bytes: 8_000,
|
||||
cache_read_suggest_threshold: 50_000,
|
||||
compact_suggest_cooldown_secs: 3_600,
|
||||
};
|
||||
run_projects_list(&ctx).await
|
||||
}
|
||||
@@ -218,6 +221,9 @@ mod tests {
|
||||
gateway_port: None,
|
||||
last_matrix_event_ms: Arc::new(AtomicI64::new(chrono::Utc::now().timestamp_millis())),
|
||||
model: None,
|
||||
compact_seed_max_bytes: 8_000,
|
||||
cache_read_suggest_threshold: 50_000,
|
||||
compact_suggest_cooldown_secs: 3_600,
|
||||
};
|
||||
let response = run_projects_list(&ctx).await;
|
||||
assert!(
|
||||
|
||||
@@ -125,6 +125,7 @@ mod tests {
|
||||
sender: "@alice:example.com".to_string(),
|
||||
content: "previous message".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
m
|
||||
|
||||
@@ -133,6 +133,7 @@ pub(super) async fn handle_llm_message(
|
||||
Ok(ClaudeCodeResult {
|
||||
messages,
|
||||
session_id,
|
||||
..
|
||||
}) => {
|
||||
let reply = if !remaining.is_empty() {
|
||||
let _ = msg_tx.send(remaining.clone());
|
||||
|
||||
@@ -513,6 +513,7 @@ mod tests {
|
||||
sender: "U01GHIJKL".to_string(),
|
||||
content: "previous message".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
m
|
||||
|
||||
@@ -56,6 +56,7 @@ mod tests {
|
||||
content: "hi there!".to_string(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ pub(super) async fn handle_llm_message(
|
||||
Ok(ClaudeCodeResult {
|
||||
messages,
|
||||
session_id,
|
||||
..
|
||||
}) => {
|
||||
let reply = if !remaining.is_empty() {
|
||||
let _ = msg_tx.send(remaining.clone());
|
||||
|
||||
@@ -416,6 +416,7 @@ mod tests {
|
||||
sender: sender.to_string(),
|
||||
content: "previous message".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
m
|
||||
|
||||
@@ -153,6 +153,7 @@ mod tests {
|
||||
content: "hi there!".to_string(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -200,6 +201,7 @@ mod tests {
|
||||
sender: "111".to_string(),
|
||||
content: "msg1".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
history.insert(
|
||||
@@ -211,6 +213,7 @@ mod tests {
|
||||
sender: "222".to_string(),
|
||||
content: "msg2".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user