wire reminder ops into the in-agent socket dispatch (#2635 inc 1)
This commit is contained in:
parent
174e277340
commit
ecd9030305
6 changed files with 178 additions and 45 deletions
|
|
@ -1,9 +1,14 @@
|
|||
//! In-agent socket server (loose-ends v2). Binds the harness-owned
|
||||
//! `HIVE_AGENT_SOCKET` and serves the `hive-agent-sock` protocol to the
|
||||
//! in-container producers (matrix / bash daemons, forge-notify). Todo ops
|
||||
//! hit the harness-local [`Todos`] store; a new-or-changed upsert fires an
|
||||
//! in-process [`Notify`] so the serve loop drives a turn — no hive-c0re
|
||||
//! round-trip, no broker long-poll, no marker files.
|
||||
//! In-agent socket server (loose-ends v2 + #2635 reminders). Binds the
|
||||
//! harness-owned `HIVE_AGENT_SOCKET` and serves the `hive-agent-sock`
|
||||
//! protocol to the in-container producers (matrix / bash daemons,
|
||||
//! forge-notify) and to `hive-agent-mcp`'s `remind`/`get_loose_ends`/
|
||||
//! `cancel_loose_end` tool impls. Todo ops hit the harness-local [`Todos`]
|
||||
//! store; a new-or-changed upsert fires an in-process [`Notify`] so the
|
||||
//! serve loop drives a turn. Reminder ops hit the harness-local
|
||||
//! [`Reminders`] store (`None` when the store failed to open — every
|
||||
//! reminder op then returns `Response::Err`); a reminder *firing* is a
|
||||
//! separate path (`reminder_timer`), not driven through this socket. No
|
||||
//! hive-c0re round-trip, no broker long-poll, no marker files.
|
||||
//!
|
||||
//! One request/response line per connection, matching the producers'
|
||||
//! existing best-effort JSON-line clients (they just change which socket
|
||||
|
|
@ -19,6 +24,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::reminders::{Reminder, Reminders};
|
||||
use crate::todos::{Todo, Todos};
|
||||
|
||||
/// Resolve the in-agent socket path from `HIVE_AGENT_SOCKET`. `None` when
|
||||
|
|
@ -36,7 +42,11 @@ fn socket_path() -> Option<PathBuf> {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the socket path is set but can't be bound.
|
||||
pub async fn run(store: Arc<Todos>, wake: Arc<Notify>) -> Result<()> {
|
||||
pub async fn run(
|
||||
store: Arc<Todos>,
|
||||
wake: Arc<Notify>,
|
||||
reminders: Option<Arc<Reminders>>,
|
||||
) -> Result<()> {
|
||||
let Some(path) = socket_path() else {
|
||||
tracing::info!("HIVE_AGENT_SOCKET unset — in-agent todo socket disabled");
|
||||
return Ok(());
|
||||
|
|
@ -48,8 +58,9 @@ pub async fn run(store: Arc<Todos>, wake: Arc<Notify>) -> Result<()> {
|
|||
Ok((stream, _)) => {
|
||||
let store = store.clone();
|
||||
let wake = wake.clone();
|
||||
let reminders = reminders.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(stream, &store, &wake).await {
|
||||
if let Err(e) = handle_conn(stream, &store, &wake, reminders.as_deref()).await {
|
||||
tracing::warn!(error = ?e, "in-agent todo connection failed");
|
||||
}
|
||||
});
|
||||
|
|
@ -75,7 +86,12 @@ fn bind(path: &Path) -> Result<UnixListener> {
|
|||
|
||||
/// Handle one connection: read a single JSON request line, apply it to the
|
||||
/// store, write the JSON response line back.
|
||||
async fn handle_conn(stream: UnixStream, store: &Todos, wake: &Notify) -> Result<()> {
|
||||
async fn handle_conn(
|
||||
stream: UnixStream,
|
||||
store: &Todos,
|
||||
wake: &Notify,
|
||||
reminders: Option<&Reminders>,
|
||||
) -> Result<()> {
|
||||
let (read, mut write) = stream.into_split();
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut line = String::new();
|
||||
|
|
@ -83,7 +99,7 @@ async fn handle_conn(stream: UnixStream, store: &Todos, wake: &Notify) -> Result
|
|||
return Ok(());
|
||||
}
|
||||
let resp = match serde_json::from_str::<Request>(line.trim()) {
|
||||
Ok(req) => dispatch(req, store, wake),
|
||||
Ok(req) => dispatch(req, store, wake, reminders),
|
||||
Err(e) => Response::Err {
|
||||
message: format!("bad request: {e}"),
|
||||
},
|
||||
|
|
@ -96,8 +112,9 @@ async fn handle_conn(stream: UnixStream, store: &Todos, wake: &Notify) -> Result
|
|||
}
|
||||
|
||||
/// Apply one request to the store, firing `wake` on a new/changed upsert so
|
||||
/// the serve loop runs a turn.
|
||||
fn dispatch(req: Request, store: &Todos, wake: &Notify) -> Response {
|
||||
/// the serve loop runs a turn. `reminders` is `None` when that store
|
||||
/// failed to open at boot — every reminder op then returns an `Err`.
|
||||
fn dispatch(req: Request, store: &Todos, wake: &Notify, reminders: Option<&Reminders>) -> Response {
|
||||
match req {
|
||||
Request::UpsertTodo {
|
||||
subsystem,
|
||||
|
|
@ -142,6 +159,78 @@ fn dispatch(req: Request, store: &Todos, wake: &Notify) -> Response {
|
|||
},
|
||||
Err(e) => err(&e),
|
||||
},
|
||||
Request::StoreReminder {
|
||||
message,
|
||||
timing,
|
||||
file_path,
|
||||
} => match reminders {
|
||||
Some(r) => {
|
||||
match crate::reminder_timer::store(r, &message, &timing, file_path.as_deref()) {
|
||||
Ok(_id) => Response::Ok,
|
||||
Err(message) => Response::Err { message },
|
||||
}
|
||||
}
|
||||
None => no_reminders_store(),
|
||||
},
|
||||
Request::ListReminders => match reminders {
|
||||
Some(r) => match r.list_pending() {
|
||||
Ok(rows) => Response::LooseEnds {
|
||||
loose_ends: rows.into_iter().map(reminder_to_loose_end).collect(),
|
||||
},
|
||||
Err(e) => err(&e),
|
||||
},
|
||||
None => no_reminders_store(),
|
||||
},
|
||||
Request::CancelReminder { id } => match reminders {
|
||||
Some(r) => match r.cancel(id) {
|
||||
Ok(count) => Response::Acked {
|
||||
count: u64::try_from(count).unwrap_or(0),
|
||||
},
|
||||
Err(e) => err(&e),
|
||||
},
|
||||
None => no_reminders_store(),
|
||||
},
|
||||
Request::CountPendingReminders => match reminders {
|
||||
Some(r) => match r.count_pending() {
|
||||
Ok(count) => Response::PendingRemindersCount { count },
|
||||
Err(e) => err(&e),
|
||||
},
|
||||
None => no_reminders_store(),
|
||||
},
|
||||
Request::ReminderRollup { since_secs } => match reminders {
|
||||
Some(r) => {
|
||||
let since_secs = i64::try_from(since_secs).unwrap_or(i64::MAX);
|
||||
match r.rollup(since_secs) {
|
||||
Ok(stats) => Response::ReminderRollup { stats },
|
||||
Err(e) => err(&e),
|
||||
}
|
||||
}
|
||||
None => no_reminders_store(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared "reminders db unavailable" response for every reminder op when
|
||||
/// the store failed to open at boot (see `main.rs`'s best-effort open).
|
||||
fn no_reminders_store() -> Response {
|
||||
Response::Err {
|
||||
message: "reminders store unavailable on this agent".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a stored [`Reminder`] to a [`LooseEnd::Reminder`], deriving
|
||||
/// `age_seconds` from `created_at` (mirrors the old c0re rendering —
|
||||
/// "age" is how long the reminder has been *scheduled*, not how soon
|
||||
/// it's due).
|
||||
fn reminder_to_loose_end(r: Reminder) -> LooseEnd {
|
||||
let now = hive_sh4re::wire_time::now_unix();
|
||||
let age = u64::try_from(now.saturating_sub(r.created_at)).unwrap_or(0);
|
||||
LooseEnd::Reminder {
|
||||
id: r.id,
|
||||
owner: crate::identity::label(),
|
||||
message: r.message,
|
||||
due_at: hive_sh4re::wire_time::from_secs(r.due_at),
|
||||
age_seconds: age,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue