more reorg
This commit is contained in:
parent
fcd77fb79e
commit
390b6c6c0a
8 changed files with 117 additions and 133 deletions
107
src/user/log.rs
107
src/user/log.rs
|
|
@ -1,107 +0,0 @@
|
|||
// log.rs — Persistent conversation log
|
||||
//
|
||||
// Append-only JSONL file that records every message in the conversation.
|
||||
// This is the permanent record — never truncated, never compacted.
|
||||
// The in-memory message array is a view into this log; compaction
|
||||
// builds that view by mixing raw recent messages with journal
|
||||
// summaries of older ones.
|
||||
//
|
||||
// Each line is a JSON-serialized Message with its timestamp.
|
||||
// The log survives session restarts, compactions, and crashes.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::agent::context::ConversationEntry;
|
||||
|
||||
pub struct ConversationLog {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl ConversationLog {
|
||||
pub fn new(path: PathBuf) -> Result<Self> {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("creating log dir {}", parent.display()))?;
|
||||
}
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
/// Append a conversation entry to the log.
|
||||
pub fn append(&self, entry: &ConversationEntry) -> Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)
|
||||
.with_context(|| format!("opening log {}", self.path.display()))?;
|
||||
|
||||
let line = serde_json::to_string(entry)
|
||||
.context("serializing entry for log")?;
|
||||
writeln!(file, "{}", line)
|
||||
.context("writing to conversation log")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the tail of the log (last `max_bytes` bytes).
|
||||
/// Seeks to `file_len - max_bytes`, skips the first partial line,
|
||||
/// then parses forward. For logs smaller than `max_bytes`, reads everything.
|
||||
pub fn read_tail(&self, max_bytes: u64) -> Result<Vec<ConversationEntry>> {
|
||||
if !self.path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let file = File::open(&self.path)
|
||||
.with_context(|| format!("opening log {}", self.path.display()))?;
|
||||
let file_len = file.metadata()?.len();
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
if file_len > max_bytes {
|
||||
reader.seek(SeekFrom::Start(file_len - max_bytes))?;
|
||||
// Skip partial first line
|
||||
let mut discard = String::new();
|
||||
reader.read_line(&mut discard)?;
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line.context("reading log tail")?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Try ConversationEntry first (new format), fall back to bare Message (old logs)
|
||||
if let Ok(entry) = serde_json::from_str::<ConversationEntry>(line) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Get the timestamp of the oldest message in the log.
|
||||
pub fn oldest_timestamp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
let file = File::open(&self.path).ok()?;
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines().flatten() {
|
||||
let line = line.trim().to_string();
|
||||
if line.is_empty() { continue; }
|
||||
if let Ok(entry) = serde_json::from_str::<ConversationEntry>(&line) {
|
||||
if let Some(ts) = &entry.message().timestamp {
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts) {
|
||||
return Some(dt.to_utc());
|
||||
}
|
||||
// Try other formats
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S") {
|
||||
return Some(dt.and_utc());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
// machine, DMN, identity) lives in mind/.
|
||||
|
||||
pub mod ui_channel;
|
||||
pub mod log;
|
||||
|
||||
pub mod chat;
|
||||
pub mod context;
|
||||
|
|
@ -725,7 +724,7 @@ pub async fn main() {
|
|||
|
||||
match &cli.command {
|
||||
Some(SubCmd::Read { follow, block }) => {
|
||||
if let Err(e) = crate::mind::observe::cmd_read_inner(*follow, *block, cli.debug).await {
|
||||
if let Err(e) = crate::mind::log::cmd_read_inner(*follow, *block, cli.debug).await {
|
||||
eprintln!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
@ -737,7 +736,7 @@ pub async fn main() {
|
|||
eprintln!("Usage: consciousness write <message>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Err(e) = crate::mind::observe::cmd_write(&msg, cli.debug).await {
|
||||
if let Err(e) = crate::mind::log::cmd_write(&msg, cli.debug).await {
|
||||
eprintln!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue