Save all context sections (system, identity, journal, conversation) to per-agent log files for both subconscious and unconscious agents. Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
138 lines
4.4 KiB
Rust
138 lines
4.4 KiB
Rust
// channel_log.rs — Per-channel message history
|
|
//
|
|
// Shared by all channel daemon implementations. Tracks messages
|
|
// with consumed/unread semantics for the recv protocol.
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
const DEFAULT_CAPACITY: usize = 200;
|
|
|
|
/// Per-channel message history with consumed/unread tracking.
|
|
pub struct ChannelLog {
|
|
messages: VecDeque<String>,
|
|
consumed: usize,
|
|
total: usize,
|
|
/// Messages we sent (don't count as unread)
|
|
own: usize,
|
|
}
|
|
|
|
impl ChannelLog {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
messages: VecDeque::with_capacity(DEFAULT_CAPACITY),
|
|
consumed: 0,
|
|
total: 0,
|
|
own: 0,
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, line: String) {
|
|
if self.messages.len() >= DEFAULT_CAPACITY {
|
|
self.messages.pop_front();
|
|
if self.consumed > 0 {
|
|
self.consumed -= 1;
|
|
}
|
|
}
|
|
self.messages.push_back(line);
|
|
self.total += 1;
|
|
}
|
|
|
|
pub fn unread(&self) -> u32 {
|
|
(self.total - self.consumed - self.own) as u32
|
|
}
|
|
|
|
/// Push a message that we sent (doesn't count as unread).
|
|
pub fn push_own(&mut self, line: String) {
|
|
self.push(line);
|
|
self.own += 1;
|
|
}
|
|
|
|
/// Return all unconsumed messages (marks consumed), plus scrollback
|
|
/// to reach at least min_count total.
|
|
pub fn recv_new(&mut self, min_count: usize) -> String {
|
|
let buf_len = self.messages.len();
|
|
let unconsumed_start = buf_len.saturating_sub(self.total - self.consumed);
|
|
|
|
let new_msgs: Vec<&str> = self.messages.iter()
|
|
.skip(unconsumed_start)
|
|
.map(|s| s.as_str())
|
|
.collect();
|
|
|
|
let need_extra = min_count.saturating_sub(new_msgs.len());
|
|
let scroll_start = unconsumed_start.saturating_sub(need_extra);
|
|
let scrollback: Vec<&str> = self.messages.iter()
|
|
.skip(scroll_start)
|
|
.take(unconsumed_start - scroll_start)
|
|
.map(|s| s.as_str())
|
|
.collect();
|
|
|
|
self.consumed = self.total;
|
|
self.own = 0;
|
|
|
|
let mut result = scrollback;
|
|
result.extend(new_msgs);
|
|
result.join("\n")
|
|
}
|
|
|
|
/// Return last N lines without consuming.
|
|
pub fn recv_history(&self, count: usize) -> String {
|
|
let start = self.messages.len().saturating_sub(count);
|
|
self.messages.range(start..)
|
|
.map(|s| s.as_str())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
}
|
|
|
|
// ── Disk logging ───────────────────────────────────────────────
|
|
|
|
use std::path::PathBuf;
|
|
|
|
/// Append a timestamped message to a per-channel log file.
|
|
///
|
|
/// Log path: `{log_dir}/{channel}.log`
|
|
/// Format: `{unix_secs} <{nick}> {text}`
|
|
pub fn append_disk_log(log_dir: &std::path::Path, channel: &str, nick: &str, text: &str) {
|
|
use std::io::Write;
|
|
let filename = format!("{}.log", channel.trim_start_matches('#').to_lowercase());
|
|
let _ = std::fs::create_dir_all(log_dir);
|
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(log_dir.join(&filename))
|
|
{
|
|
let secs = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
let _ = writeln!(f, "{secs} <{nick}> {text}");
|
|
}
|
|
}
|
|
|
|
/// Load a ChannelLog from a disk log file, populating with the last N lines.
|
|
pub fn load_disk_log(log_dir: &std::path::Path, channel: &str) -> ChannelLog {
|
|
use std::io::{BufRead, BufReader};
|
|
let filename = format!("{}.log", channel.trim_start_matches('#').to_lowercase());
|
|
let path = log_dir.join(&filename);
|
|
|
|
let mut log = ChannelLog::new();
|
|
let Ok(file) = std::fs::File::open(&path) else { return log };
|
|
let reader = BufReader::new(file);
|
|
|
|
// Read all lines, keep only the last DEFAULT_CAPACITY
|
|
let lines: Vec<String> = reader.lines().flatten().collect();
|
|
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;
|
|
log
|
|
}
|
|
|
|
/// Standard log directory for a channel daemon.
|
|
pub fn log_dir(daemon_name: &str) -> PathBuf {
|
|
dirs::home_dir()
|
|
.unwrap_or_default()
|
|
.join(format!(".consciousness/channels/{}.logs", daemon_name))
|
|
}
|