From b4dfd3c092a3438ce83f84b16efd7900219c65a2 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 03:28:00 -0400 Subject: [PATCH] Log full agent context window on completion Save all context sections (system, identity, journal, conversation) to per-agent log files for both subconscious and unconscious agents. Co-Authored-By: ProofOfConcept --- channels/irc/src/main.rs | 46 ++++++++++++++++++++++++++++++++----- src/mind/subconscious.rs | 1 + src/mind/unconscious.rs | 13 +++++++---- src/thalamus/channel_log.rs | 10 ++++---- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/channels/irc/src/main.rs b/channels/irc/src/main.rs index 48c60e0..ee94e89 100644 --- a/channels/irc/src/main.rs +++ b/channels/irc/src/main.rs @@ -191,9 +191,13 @@ impl State { fn push_message(&mut self, line: String, urgency: u8, channel: &str) { // Store in per-channel log + let ch = channel.to_string(); self.channel_logs - .entry(channel.to_string()) - .or_insert_with(ChannelLog::new) + .entry(ch.clone()) + .or_insert_with(|| { + let target = channel_to_target(&ch); + channel_log::load_disk_log(&log_dir(), &target) + }) .push(line.clone()); // Notify all subscribers @@ -221,7 +225,34 @@ impl State { } async fn send_privmsg(&mut self, target: &str, msg: &str) -> io::Result<()> { - self.send_raw(&format!("PRIVMSG {target} :{msg}")).await + // IRC max line = 512 bytes including CRLF. The server prepends + // our prefix when relaying: ":nick!~user@host PRIVMSG target :msg\r\n" + // User is often ~nick (nick_len + 1). Host is up to 63 bytes. + let nick_len = self.config.nick.len(); + let overhead = 1 + nick_len + 2 + nick_len + 1 + 63 + + " PRIVMSG ".len() + target.len() + " :".len() + 2; + let max_msg = 512_usize.saturating_sub(overhead); + + if max_msg == 0 { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "target too long")); + } + + // Split on UTF-8 char boundaries + let mut remaining = msg; + while !remaining.is_empty() { + let split_at = if remaining.len() <= max_msg { + remaining.len() + } else { + // Find last char boundary at or before max_msg + let mut i = max_msg; + while i > 0 && !remaining.is_char_boundary(i) { i -= 1; } + if i == 0 { max_msg } else { i } + }; + let (chunk, rest) = remaining.split_at(split_at); + self.send_raw(&format!("PRIVMSG {target} :{chunk}")).await?; + remaining = rest; + } + Ok(()) } } @@ -389,11 +420,11 @@ async fn register_and_read( if let Err(e) = state.borrow_mut().send_raw(&format!("JOIN {ch}")).await { warn!("irc: failed to join {ch}: {e}"); } - // Create log entry so channel appears in list() + // Load history from disk so recv has scrollback let key = format!("irc.{ch}"); state.borrow_mut().channel_logs .entry(key) - .or_insert_with(ChannelLog::new); + .or_insert_with(|| channel_log::load_disk_log(&log_dir(), ch)); } } @@ -536,7 +567,10 @@ impl channel_server::Server for ChannelServerImpl { }; state.borrow_mut().channel_logs .entry(channel.clone()) - .or_insert_with(ChannelLog::new) + .or_insert_with(|| { + let target = channel_to_target(&channel); + channel_log::load_disk_log(&log_dir(), &target) + }) .push_own(log_line); Ok(()) diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index 6e85081..d10f9e1 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -617,6 +617,7 @@ impl Subconscious { self.agents[idx].handle = Some(tokio::spawn(async move { let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await; + super::unconscious::save_agent_log(&auto.name, &forked).await; (auto, result) })); } diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index 009e80f..a7e9e0a 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -296,17 +296,22 @@ impl Unconscious { } } -async fn save_agent_log(name: &str, agent: &std::sync::Arc) { +pub async fn save_agent_log(name: &str, agent: &std::sync::Arc) { let dir = dirs::home_dir().unwrap_or_default() .join(format!(".consciousness/logs/{}", name)); if std::fs::create_dir_all(&dir).is_err() { return; } let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S"); let path = dir.join(format!("{}.json", ts)); - let nodes: Vec = { + let sections: serde_json::Value = { let ctx = agent.context.lock().await; - ctx.conversation().to_vec() + serde_json::json!({ + "system": ctx.system(), + "identity": ctx.identity(), + "journal": ctx.journal(), + "conversation": ctx.conversation(), + }) }; - if let Ok(json) = serde_json::to_string_pretty(&nodes) { + if let Ok(json) = serde_json::to_string_pretty(§ions) { let _ = std::fs::write(&path, json); dbglog!("[unconscious] saved log to {}", path.display()); } diff --git a/src/thalamus/channel_log.rs b/src/thalamus/channel_log.rs index 298f17d..281446d 100644 --- a/src/thalamus/channel_log.rs +++ b/src/thalamus/channel_log.rs @@ -76,9 +76,8 @@ impl ChannelLog { /// Return last N lines without consuming. pub fn recv_history(&self, count: usize) -> String { - self.messages.iter() - .rev().take(count) - .collect::>().into_iter().rev() + let start = self.messages.len().saturating_sub(count); + self.messages.range(start..) .map(|s| s.as_str()) .collect::>() .join("\n") @@ -122,8 +121,9 @@ pub fn load_disk_log(log_dir: &std::path::Path, channel: &str) -> ChannelLog { // Read all lines, keep only the last DEFAULT_CAPACITY let lines: Vec = reader.lines().flatten().collect(); - for line in lines.into_iter().rev().take(DEFAULT_CAPACITY).collect::>().into_iter().rev() { - log.push(line); + let start = lines.len().saturating_sub(DEFAULT_CAPACITY); + for line in &lines[start..] { + log.push(line.clone()); } // Mark all loaded lines as consumed (they're history, not new) log.consumed = log.total;