Plan M5a: local memory in 15 tasks
memoryd (files, the check, edits, journal, index, recall, serving), the memory tools and built-in grants in brokerd, the memory block in loopd, bxctl reindex and memory, and the device checks with memoryd. Each task was built as a reference and gated. Each was replayed on its own at its end state (854 to 936 tests) and mutation-checked. Each skeleton compiles and fails its tests. Every todo comment was read beside its signature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
//! Serving: three sockets, one request per connection, each answered by one frame (M5a spec,
|
||||
//! section 7). One lock covers the files and the index, so writes never interleave.
|
||||
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use proto::{
|
||||
Empty, Envelope, ErrorCode, MemoryAdminReply, MemoryArgs, MemoryBlockReply, MemoryCall,
|
||||
MemoryCommand, Message, PROTOCOL_VERSION, RecallArgs, Reindexed, Timestamp, ToolResponse,
|
||||
WireError, read_frame, write_frame,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::edit::{self, Context};
|
||||
use crate::files::Store;
|
||||
use crate::index::{self, Index};
|
||||
use crate::recall::Recalled;
|
||||
use crate::{journal, recall, review};
|
||||
|
||||
/// How long a peer may take to send its request.
|
||||
pub const READ_WAIT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Where log lines go: stderr in `main`, a buffer in tests.
|
||||
pub type Log = Arc<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
struct State {
|
||||
index: Index,
|
||||
}
|
||||
|
||||
pub struct Memoryd {
|
||||
config: Config,
|
||||
store: Store,
|
||||
state: Mutex<State>,
|
||||
log: Log,
|
||||
}
|
||||
|
||||
/// A started `memoryd`: its three listeners, bound and waiting.
|
||||
pub struct Running {
|
||||
memoryd: Arc<Memoryd>,
|
||||
listeners: Vec<(UnixListener, Which)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Which {
|
||||
Loop,
|
||||
Broker,
|
||||
Admin,
|
||||
}
|
||||
|
||||
/// Create what is missing under `memory/`, open the index and catch it up, then bind the three
|
||||
/// sockets. The error names what failed; the caller points at `memoryd-start-failed`.
|
||||
pub fn start(config: Config, log: Log) -> Result<Running, String> {
|
||||
// 1. `Store::new(&config.memory_dir())`, then `store.init(&config.memory.soul_starter)?`.
|
||||
// 2. `Index::open(&config.index_path(), config.index.max_class)?`, then
|
||||
// `index.catch_up(&config.sessions_dir(), &store.path("notes.md"))?`; log each problem as
|
||||
// format!("memoryd: not indexed: {problem}").
|
||||
// 3. `crate::socket::listen` the loop, broker and admin sockets of the config, in that order,
|
||||
// paired with Which::Loop, Which::Broker and Which::Admin.
|
||||
// 4. Ok(Running { memoryd: Arc::new(Memoryd { config, store, state: Mutex::new(State { index
|
||||
// }), log }), listeners }).
|
||||
todo!()
|
||||
}
|
||||
|
||||
impl Running {
|
||||
/// Accept on every socket, one thread per connection, until a listener fails.
|
||||
pub fn run(self) -> Result<(), String> {
|
||||
// An mpsc channel of String. For each (listener, which): spawn a thread (with
|
||||
// `std::thread::Builder::new().spawn`, its error -> Err(format!("cannot start a thread:
|
||||
// {e}"))) that loops over `listener.incoming()`: Ok(stream) -> spawn one more thread
|
||||
// running `memoryd.connection(stream, which)`, and if that spawn fails, log
|
||||
// format!("memoryd: cannot start a thread: {e}") and go on; Err(e) -> send format!("a
|
||||
// listener failed: {e}") and return. Drop the sender, then wait on `rx.recv()`: a message
|
||||
// -> Err(it); all senders gone -> Ok(()).
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn error(detail: String) -> Message {
|
||||
Message::Error(WireError {
|
||||
code: ErrorCode::Internal,
|
||||
detail,
|
||||
})
|
||||
}
|
||||
|
||||
fn bad(detail: &str) -> Message {
|
||||
Message::Error(WireError {
|
||||
code: ErrorCode::BadMessage,
|
||||
detail: detail.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Memoryd {
|
||||
fn lock(&self) -> MutexGuard<'_, State> {
|
||||
self.state.lock().unwrap_or_else(|p| p.into_inner())
|
||||
}
|
||||
|
||||
fn connection(&self, mut stream: UnixStream, which: Which) {
|
||||
// `set_read_timeout(Some(READ_WAIT))` (ignore its result). `read_frame`; on error just
|
||||
// return. The reply is `self.handle(request.msg, which)`, written with `write_frame` in an
|
||||
// Envelope with v PROTOCOL_VERSION, the request's id, final true (ignore the result).
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// The answer to one request on one socket. A message sent to the wrong socket is refused.
|
||||
fn handle(&self, msg: Message, which: Which) -> Message {
|
||||
match (which, msg) {
|
||||
(Which::Loop, Message::MemoryBlock(_)) => self.block(),
|
||||
(Which::Loop, Message::SessionUpdated(update)) => {
|
||||
self.session_updated(update.session.as_str())
|
||||
}
|
||||
(Which::Broker, Message::MemoryCall(call)) => Message::ToolResponse(self.call(call)),
|
||||
(Which::Admin, Message::Reindex(_)) => self.reindex(),
|
||||
(Which::Admin, Message::MemoryAdmin(admin)) => self.admin(admin.command, &admin.arg),
|
||||
_ => bad("memoryd does not take this message on this socket"),
|
||||
}
|
||||
}
|
||||
|
||||
fn block(&self) -> Message {
|
||||
// Hold the lock. `self.store.block()`: Ok(block) -> log each (file, refused) of
|
||||
// block.left_out as format!("memoryd: an entry of {} was left out of the block: {}",
|
||||
// file.name(), refused.sentence()), and answer Message::MemoryBlockReply(MemoryBlockReply {
|
||||
// text: block.text }); Err(e) -> error(e).
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn session_updated(&self, session: &str) -> Message {
|
||||
// Hold the lock. `index::session_logs(&self.config.sessions_dir(), session)`; its error ->
|
||||
// error(e). For each (epoch, path): `state.index.index_log(path, session, *epoch)`, and log
|
||||
// an error as format!("memoryd: not indexed: {e}"). Answer Message::Ok(Empty {}).
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// A decided call from `brokerd`: `recall` or `memory`, under the one lock. `Failed` carries the
|
||||
/// sentence for the model.
|
||||
fn call(&self, call: MemoryCall) -> ToolResponse {
|
||||
// Hold the lock. By the request's tool: proto::memory::RECALL -> `self.recall(&state,
|
||||
// &call)`; proto::memory::MEMORY -> `self.memory(&state, &call)`; anything else ->
|
||||
// Err("memoryd runs only recall and memory"). Ok(found) -> ToolResponse::Result { content:
|
||||
// found.text, class: found.class, untrusted: found.untrusted, truncated: false };
|
||||
// Err(message) -> ToolResponse::Failed { message }.
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `recall`: search, labelled by what it found.
|
||||
fn recall(&self, state: &State, call: &MemoryCall) -> Result<Recalled, String> {
|
||||
// Parse the request's arguments as RecallArgs (an error -> format!("recall's arguments:
|
||||
// {e}")), then `recall::recall(&state.index, &self.store, &args, session,
|
||||
// Timestamp::now())`.
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `memory`: staged for review, or applied and journalled; then what it appended to `notes.md`
|
||||
/// is indexed. Its answer is labelled private and trusted: it is `memoryd`'s own sentence.
|
||||
fn memory(&self, state: &State, call: &MemoryCall) -> Result<Recalled, String> {
|
||||
// 1. Parse the arguments as MemoryArgs (an error -> format!("memory's arguments: {e}")).
|
||||
// 2. A Context: session, call (request.call.0), taint and untrusted from the MemoryCall,
|
||||
// now Timestamp::now().
|
||||
// 3. When `self.config.memory.review` is on, the action is not a Note, and the call is not
|
||||
// untrusted: `review::submit(&self.store, &args, &request.arguments, &ctx)`. Otherwise
|
||||
// `edit::apply(&self.store, &args, &ctx)`, then `journal::record(&self.store, &ctx,
|
||||
// &outcome)?`, answering outcome.answer.
|
||||
// 4. `self.index_notes(state)`, whatever the answer.
|
||||
// 5. The answer as a Recalled { text, class Private, untrusted false }.
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Index what was appended to `notes.md`; a failure is logged, and the next start catches up.
|
||||
fn index_notes(&self, state: &State) {
|
||||
// Read `self.store.path("notes.md")` (an error names the path) and
|
||||
// `state.index.index_notes(&text)`; log an error as format!("memoryd: notes not indexed:
|
||||
// {e}").
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn reindex(&self) -> Message {
|
||||
// Hold the lock (mutably). `index::rebuild(&self.config.index_path(), max_class,
|
||||
// &sessions_dir, &self.store.path("notes.md"))`; its error -> error(e); log each problem as
|
||||
// format!("memoryd: not indexed: {problem}"). Then `Index::open` the same path again and
|
||||
// put it in `state.index` (the old connection still points at the replaced file); its error
|
||||
// -> error(e). Answer Message::Reindexed { logs, records, notes } from the counts.
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn admin(&self, command: MemoryCommand, arg: &str) -> Message {
|
||||
// Hold the lock; now = Timestamp::now(). Log: an empty arg means 10, otherwise it must
|
||||
// parse as usize, else Err(format!("log takes a count; {arg:?} is not one")); then
|
||||
// `journal::log(&self.store, n)`. Undo -> `journal::undo(&self.store, arg, now)`. Pending
|
||||
// -> `review::list`. Accept -> `review::accept(.., arg, now)`. Reject ->
|
||||
// `review::reject(.., arg, now)`. Then `self.index_notes(&state)`. Ok(text) ->
|
||||
// Message::MemoryAdminReply(MemoryAdminReply { text }); Err(e) -> error(e).
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user