per-channel message logs with shared ChannelLog type
Move ChannelLog to src/thalamus/channel_log.rs — shared by all channel daemon implementations. Each channel/PM gets its own log with consumed/unread tracking. IRC daemon: channels tracked via BTreeMap<String, ChannelLog>. list() returns all channels (joined + PMs) with per-channel unread counts. Sent messages stored in logs too. Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
8e66f0a66c
commit
e604659e3a
3 changed files with 102 additions and 65 deletions
|
|
@ -161,14 +161,12 @@ impl AsyncWriter for PlainWriter {
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
use poc_memory::thalamus::channel_log::ChannelLog;
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
config: Config,
|
config: Config,
|
||||||
/// Ring buffer of formatted message lines (all channels interleaved)
|
/// Per-channel message logs (keyed by channel path, e.g. "irc.#bcachefs")
|
||||||
messages: VecDeque<String>,
|
channel_logs: std::collections::BTreeMap<String, ChannelLog>,
|
||||||
/// Number of messages consumed (monotonic)
|
|
||||||
consumed: usize,
|
|
||||||
/// Total messages ever received (monotonic)
|
|
||||||
total: usize,
|
|
||||||
/// Currently joined channels
|
/// Currently joined channels
|
||||||
channels: Vec<String>,
|
channels: Vec<String>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
|
|
@ -185,9 +183,7 @@ impl State {
|
||||||
let channels = config.channels.clone();
|
let channels = config.channels.clone();
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
messages: VecDeque::with_capacity(MAX_HISTORY),
|
channel_logs: std::collections::BTreeMap::new(),
|
||||||
consumed: 0,
|
|
||||||
total: 0,
|
|
||||||
channels,
|
channels,
|
||||||
connected: false,
|
connected: false,
|
||||||
writer: None,
|
writer: None,
|
||||||
|
|
@ -196,14 +192,11 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_message(&mut self, line: String, urgency: u8, channel: &str) {
|
fn push_message(&mut self, line: String, urgency: u8, channel: &str) {
|
||||||
if self.messages.len() >= MAX_HISTORY {
|
// Store in per-channel log
|
||||||
self.messages.pop_front();
|
self.channel_logs
|
||||||
if self.consumed > 0 {
|
.entry(channel.to_string())
|
||||||
self.consumed -= 1;
|
.or_insert_with(ChannelLog::new)
|
||||||
}
|
.push(line.clone());
|
||||||
}
|
|
||||||
self.messages.push_back(line.clone());
|
|
||||||
self.total += 1;
|
|
||||||
|
|
||||||
// Notify all subscribers
|
// Notify all subscribers
|
||||||
let preview = line.chars().take(80).collect::<String>();
|
let preview = line.chars().take(80).collect::<String>();
|
||||||
|
|
@ -221,42 +214,6 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
let mut result = scrollback;
|
|
||||||
result.extend(new_msgs);
|
|
||||||
result.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_history(&self, count: usize) -> String {
|
|
||||||
self.messages.iter()
|
|
||||||
.rev()
|
|
||||||
.take(count)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.rev()
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_raw(&mut self, line: &str) -> io::Result<()> {
|
async fn send_raw(&mut self, line: &str) -> io::Result<()> {
|
||||||
if let Some(ref mut w) = self.writer {
|
if let Some(ref mut w) = self.writer {
|
||||||
w.write_line(line).await
|
w.write_line(line).await
|
||||||
|
|
@ -536,14 +493,16 @@ impl channel_server::Server for ChannelServerImpl {
|
||||||
mut results: channel_server::RecvResults,
|
mut results: channel_server::RecvResults,
|
||||||
) -> Promise<(), capnp::Error> {
|
) -> Promise<(), capnp::Error> {
|
||||||
let params = pry!(params.get());
|
let params = pry!(params.get());
|
||||||
let _channel = pry!(params.get_channel());
|
let channel = pry!(pry!(params.get_channel()).to_str()).to_string();
|
||||||
let all_new = params.get_all_new();
|
let all_new = params.get_all_new();
|
||||||
let min_count = params.get_min_count() as usize;
|
let min_count = params.get_min_count() as usize;
|
||||||
|
|
||||||
let text = if all_new {
|
let mut s = self.state.borrow_mut();
|
||||||
self.state.borrow_mut().recv_new(min_count)
|
let text = match s.channel_logs.get_mut(&channel) {
|
||||||
} else {
|
Some(log) => {
|
||||||
self.state.borrow().recv_history(min_count)
|
if all_new { log.recv_new(min_count) } else { log.recv_history(min_count) }
|
||||||
|
}
|
||||||
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
results.get().set_text(&text);
|
results.get().set_text(&text);
|
||||||
|
|
@ -603,17 +562,18 @@ impl channel_server::Server for ChannelServerImpl {
|
||||||
mut results: channel_server::ListResults,
|
mut results: channel_server::ListResults,
|
||||||
) -> Promise<(), capnp::Error> {
|
) -> Promise<(), capnp::Error> {
|
||||||
let s = self.state.borrow();
|
let s = self.state.borrow();
|
||||||
let channels = &s.channels;
|
|
||||||
let unread = (s.total - s.consumed) as u32;
|
|
||||||
let connected = s.connected;
|
let connected = s.connected;
|
||||||
|
|
||||||
let mut list = results.get().init_channels(channels.len() as u32);
|
// All channels with logs (joined + PMs)
|
||||||
for (i, ch) in channels.iter().enumerate() {
|
let names: Vec<String> = s.channel_logs.keys().cloned().collect();
|
||||||
|
let mut list = results.get().init_channels(names.len() as u32);
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
let mut entry = list.reborrow().get(i as u32);
|
let mut entry = list.reborrow().get(i as u32);
|
||||||
let name = format!("irc.{ch}");
|
entry.set_name(name);
|
||||||
entry.set_name(&name);
|
|
||||||
entry.set_connected(connected);
|
entry.set_connected(connected);
|
||||||
entry.set_unread(unread);
|
entry.set_unread(
|
||||||
|
s.channel_logs.get(name).map_or(0, |l| l.unread())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise::ok(())
|
Promise::ok(())
|
||||||
|
|
|
||||||
76
src/thalamus/channel_log.rs
Normal file
76
src/thalamus/channel_log.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelLog {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
messages: VecDeque::with_capacity(DEFAULT_CAPACITY),
|
||||||
|
consumed: 0,
|
||||||
|
total: 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) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
self.messages.iter()
|
||||||
|
.rev().take(count)
|
||||||
|
.collect::<Vec<_>>().into_iter().rev()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
// code (in claude/) and the future substrate-independent consciousness
|
// code (in claude/) and the future substrate-independent consciousness
|
||||||
// binary.
|
// binary.
|
||||||
|
|
||||||
|
pub mod channel_log;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod idle;
|
pub mod idle;
|
||||||
pub mod supervisor;
|
pub mod supervisor;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue