From 58737a2cefa68fe2a5b50dcb5b7bc4d22ee09e31 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 13:17:38 -0400 Subject: [PATCH] channel_log: shared disk logging with round-trip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move IRC's disk logging to thalamus/channel_log.rs as shared functions: append_disk_log() and load_disk_log(). Any daemon can use them — opt-in, not mandatory (tmux won't use them). load_disk_log() populates a ChannelLog from disk on startup, so history survives daemon restarts. IRC daemon now uses the shared functions. Co-Authored-By: Kent Overstreet --- Cargo.lock | 13 +++++++++ channels/irc/src/main.rs | 20 ++++---------- channels/telegram/Cargo.toml | 2 +- channels/tmux/src/main.rs | 2 +- src/thalamus/channel_log.rs | 52 ++++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55201b8..1bd4a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2593,6 +2593,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -2902,6 +2903,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/channels/irc/src/main.rs b/channels/irc/src/main.rs index c0a9ad9..739113a 100644 --- a/channels/irc/src/main.rs +++ b/channels/irc/src/main.rs @@ -27,6 +27,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt; use log::{info, warn, error}; use poc_memory::channel_capnp::{channel_client, channel_server}; +use poc_memory::thalamus::channel_log; // ── Constants ────────────────────────────────────────────────── @@ -229,23 +230,12 @@ impl State { // ── Persistence ──────────────────────────────────────────────── -fn data_dir() -> PathBuf { - dirs::home_dir().unwrap_or_default().join(".consciousness/channels/irc.logs") +fn log_dir() -> PathBuf { + channel_log::log_dir("irc") } fn append_log(target: &str, nick: &str, text: &str) { - use std::io::Write; - let filename = format!("{}.log", target.trim_start_matches('#').to_lowercase()); - let dir = data_dir().join("logs"); - let _ = std::fs::create_dir_all(&dir); - if let Ok(mut f) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(dir.join(&filename)) - { - let secs = now() as u64; - let _ = writeln!(f, "{secs} <{nick}> {text}"); - } + channel_log::append_disk_log(&log_dir(), target, nick, text); } fn now() -> f64 { @@ -266,7 +256,7 @@ fn root_certs() -> rustls::RootCertStore { // ── IRC Connection Loop ──────────────────────────────────────── async fn connection_loop(state: SharedState) { - let _ = std::fs::create_dir_all(data_dir().join("logs")); + let _ = std::fs::create_dir_all(log_dir()); let mut backoff = RECONNECT_BASE_SECS; loop { diff --git a/channels/telegram/Cargo.toml b/channels/telegram/Cargo.toml index f9168c6..6705da0 100644 --- a/channels/telegram/Cargo.toml +++ b/channels/telegram/Cargo.toml @@ -9,7 +9,7 @@ capnp-rpc = "0.20" dirs = "6" futures = "0.3" poc-memory = { path = "../.." } -reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } +reqwest = { version = "0.13", default-features = false, features = ["json", "form", "rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/channels/tmux/src/main.rs b/channels/tmux/src/main.rs index 30288d6..54facf1 100644 --- a/channels/tmux/src/main.rs +++ b/channels/tmux/src/main.rs @@ -20,7 +20,7 @@ use tokio::net::UnixListener; use tokio_util::compat::TokioAsyncReadCompatExt; use log::{info, warn, error}; -use poc_memory::channel_capnp::{channel_client, channel_server}; +use poc_memory::channel_capnp::channel_server; use poc_memory::thalamus::channel_log::ChannelLog; // ── Config ───────────────────────────────────────────────────── diff --git a/src/thalamus/channel_log.rs b/src/thalamus/channel_log.rs index 3eae2e1..298f17d 100644 --- a/src/thalamus/channel_log.rs +++ b/src/thalamus/channel_log.rs @@ -84,3 +84,55 @@ impl ChannelLog { .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 = reader.lines().flatten().collect(); + for line in lines.into_iter().rev().take(DEFAULT_CAPACITY).collect::>().into_iter().rev() { + log.push(line); + } + // 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)) +}