2026-04-05 01:48:11 -04:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use std::fs::{File, OpenOptions};
|
2026-04-09 13:09:26 -04:00
|
|
|
use std::io::Write;
|
2026-04-05 01:48:11 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
WIP: Rename context_new → context, delete old files, fix UI layer
Renamed context_new.rs to context.rs, deleted context_old.rs,
types.rs, openai.rs, parsing.rs. Updated all imports. Rewrote
user/context.rs and user/widgets.rs for new types. Stubbed
working_stack tool. Killed tokenize_conv_entry.
Remaining: mind/mod.rs, mind/dmn.rs, learn.rs, chat.rs,
subconscious.rs, oneshot.rs.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 15:20:26 -04:00
|
|
|
use crate::agent::context::AstNode;
|
2026-04-09 13:09:26 -04:00
|
|
|
use crate::hippocampus::transcript::JsonlBackwardIter;
|
|
|
|
|
use memmap2::Mmap;
|
2026-04-05 01:48:11 -04:00
|
|
|
|
|
|
|
|
pub struct ConversationLog {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConversationLog {
|
|
|
|
|
pub fn new(path: PathBuf) -> Result<Self> {
|
|
|
|
|
if let Some(parent) = path.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)
|
|
|
|
|
.with_context(|| format!("creating log dir {}", parent.display()))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(Self { path })
|
|
|
|
|
}
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
pub fn append_node(&self, node: &AstNode) -> Result<()> {
|
2026-04-05 01:48:11 -04:00
|
|
|
let mut file = OpenOptions::new()
|
|
|
|
|
.create(true)
|
|
|
|
|
.append(true)
|
|
|
|
|
.open(&self.path)
|
|
|
|
|
.with_context(|| format!("opening log {}", self.path.display()))?;
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let line = serde_json::to_string(node)
|
|
|
|
|
.context("serializing node for log")?;
|
2026-04-05 01:48:11 -04:00
|
|
|
writeln!(file, "{}", line)
|
|
|
|
|
.context("writing to conversation log")?;
|
2026-04-06 23:04:08 -04:00
|
|
|
file.sync_all()
|
|
|
|
|
.context("syncing conversation log")?;
|
2026-04-05 01:48:11 -04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:09:26 -04:00
|
|
|
/// Read nodes from the tail of the log, newest first.
|
|
|
|
|
/// Caller decides when to stop (budget, count, etc).
|
|
|
|
|
pub fn read_tail(&self) -> Result<TailNodes> {
|
2026-04-05 01:48:11 -04:00
|
|
|
if !self.path.exists() {
|
2026-04-09 13:09:26 -04:00
|
|
|
anyhow::bail!("log does not exist");
|
2026-04-05 01:48:11 -04:00
|
|
|
}
|
|
|
|
|
let file = File::open(&self.path)
|
|
|
|
|
.with_context(|| format!("opening log {}", self.path.display()))?;
|
2026-04-09 13:09:26 -04:00
|
|
|
if file.metadata()?.len() == 0 {
|
|
|
|
|
anyhow::bail!("log is empty");
|
2026-04-05 01:48:11 -04:00
|
|
|
}
|
2026-04-09 13:09:26 -04:00
|
|
|
let mmap = unsafe { Mmap::map(&file)? };
|
|
|
|
|
Ok(TailNodes { _file: file, mmap })
|
2026-04-05 01:48:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn path(&self) -> &Path {
|
|
|
|
|
&self.path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn oldest_timestamp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
|
2026-04-09 13:09:26 -04:00
|
|
|
// Read forward from the start to find first timestamp
|
2026-04-05 01:48:11 -04:00
|
|
|
let file = File::open(&self.path).ok()?;
|
2026-04-09 13:09:26 -04:00
|
|
|
let mmap = unsafe { Mmap::map(&file).ok()? };
|
|
|
|
|
// Find first { ... } and parse
|
|
|
|
|
for line in mmap.split(|&b| b == b'\n') {
|
2026-04-05 01:48:11 -04:00
|
|
|
if line.is_empty() { continue; }
|
2026-04-09 13:09:26 -04:00
|
|
|
if let Ok(node) = serde_json::from_slice::<AstNode>(line) {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
if let Some(leaf) = node.leaf() {
|
|
|
|
|
if let Some(ts) = leaf.timestamp() {
|
|
|
|
|
return Some(ts);
|
2026-04-05 01:48:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 13:09:26 -04:00
|
|
|
|
|
|
|
|
/// Iterates over conversation log nodes newest-first, using mmap + backward scan.
|
|
|
|
|
pub struct TailNodes {
|
|
|
|
|
_file: File,
|
|
|
|
|
mmap: Mmap,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TailNodes {
|
|
|
|
|
pub fn iter(&self) -> impl Iterator<Item = AstNode> + '_ {
|
|
|
|
|
JsonlBackwardIter::new(&self.mmap)
|
|
|
|
|
.filter_map(|bytes| serde_json::from_slice::<AstNode>(bytes).ok())
|
|
|
|
|
}
|
|
|
|
|
}
|