hive-agent: remove the in-container questions mirror
This commit is contained in:
parent
1c4d662cf1
commit
c865114743
7 changed files with 59 additions and 528 deletions
|
|
@ -1,24 +1,20 @@
|
|||
//! In-agent socket server (loose-ends v2 + harness-local reminders +
|
||||
//! questions mirror + self-service compact). Binds the harness-owned
|
||||
//! `HIVE_AGENT_SOCKET` and serves the `hive-agent-sock` protocol to the
|
||||
//! in-container producers — matrix / bash daemons and forge-notify are the
|
||||
//! built-in ones, but any user-configured MCP server can dial the same
|
||||
//! socket and push its own todos — and to
|
||||
//! `hive-agent-mcp`'s `ask`/`answer`/`remind`/`get_loose_ends`/
|
||||
//! self-service compact). Binds the harness-owned `HIVE_AGENT_SOCKET` and
|
||||
//! serves the `hive-agent-sock` protocol to the in-container producers —
|
||||
//! matrix / bash daemons and forge-notify are the built-in ones, but any
|
||||
//! user-configured MCP server can dial the same socket and push its own
|
||||
//! todos — and to `hive-agent-mcp`'s `remind`/`get_loose_ends`/
|
||||
//! `cancel_loose_end`/`compact` 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.
|
||||
//! Question ops hit the harness-local [`Questions`] mirror the same way
|
||||
//! (`None` when it failed to open) — c0re stays the actual `Ask`/`Answer`
|
||||
//! routing + delivery rendezvous, this store only mirrors the durable
|
||||
//! "still owed a reply" view for `get_loose_ends`. `Request::Compact` is
|
||||
//! the odd one out — it doesn't touch any store, just the harness's
|
||||
//! [`Bus`] (gate-checked context usage, then the same deferred
|
||||
//! `compact_pending` flag the operator dashboard's `/compact` button
|
||||
//! sets). No hive-c0re round-trip, no broker long-poll, no marker files.
|
||||
//! `Request::Compact` is the odd one out — it doesn't touch any store, just
|
||||
//! the harness's [`Bus`] (gate-checked context usage, then the same
|
||||
//! deferred `compact_pending` flag the operator dashboard's `/compact`
|
||||
//! button sets). 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
|
||||
|
|
@ -36,7 +32,6 @@ use tokio::net::{UnixListener, UnixStream};
|
|||
use tokio::sync::Notify;
|
||||
|
||||
use crate::events::Bus;
|
||||
use crate::questions::{QuestionMirror, Questions, Role};
|
||||
use crate::reminders::{Reminder, Reminders};
|
||||
use crate::todos::{Todo, Todos};
|
||||
|
||||
|
|
@ -88,7 +83,6 @@ pub async fn run(
|
|||
store: Arc<Todos>,
|
||||
wake: Arc<Notify>,
|
||||
reminders: Option<Arc<Reminders>>,
|
||||
questions: Option<Arc<Questions>>,
|
||||
bus: Bus,
|
||||
) -> Result<()> {
|
||||
let Some(path) = socket_path() else {
|
||||
|
|
@ -103,18 +97,10 @@ pub async fn run(
|
|||
let store = store.clone();
|
||||
let wake = wake.clone();
|
||||
let reminders = reminders.clone();
|
||||
let questions = questions.clone();
|
||||
let bus = bus.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(
|
||||
stream,
|
||||
&store,
|
||||
&wake,
|
||||
reminders.as_deref(),
|
||||
questions.as_deref(),
|
||||
&bus,
|
||||
)
|
||||
.await
|
||||
if let Err(e) =
|
||||
handle_conn(stream, &store, &wake, reminders.as_deref(), &bus).await
|
||||
{
|
||||
tracing::warn!(error = ?e, "in-agent todo connection failed");
|
||||
}
|
||||
|
|
@ -159,7 +145,6 @@ async fn handle_conn(
|
|||
store: &Todos,
|
||||
wake: &Notify,
|
||||
reminders: Option<&Reminders>,
|
||||
questions: Option<&Questions>,
|
||||
bus: &Bus,
|
||||
) -> Result<()> {
|
||||
let (read, mut write) = stream.into_split();
|
||||
|
|
@ -169,7 +154,7 @@ async fn handle_conn(
|
|||
return Ok(());
|
||||
}
|
||||
let resp = match serde_json::from_str::<Request>(line.trim()) {
|
||||
Ok(req) => dispatch(req, store, wake, reminders, questions, bus),
|
||||
Ok(req) => dispatch(req, store, wake, reminders, bus),
|
||||
Err(e) => Response::Err {
|
||||
message: format!("bad request: {e}"),
|
||||
},
|
||||
|
|
@ -185,16 +170,15 @@ async fn handle_conn(
|
|||
/// the serve loop runs a turn. Each arm calls a small named handler function
|
||||
/// directly — no sub-match/`unreachable!()` indirection per family (that
|
||||
/// pattern got reviewed out of the todo family in the diagnostic-logging
|
||||
/// follow-up PR; kept the reminder and question families consistent with it
|
||||
/// here rather than reintroducing it). `reminders`/`questions` are `None` when that store failed to open at
|
||||
/// boot, in which case every op in that family returns an `Err` — each
|
||||
/// follow-up PR; kept the reminder family consistent with it here rather
|
||||
/// than reintroducing it). `reminders` is `None` when that store failed to
|
||||
/// open at boot, in which case every reminder op returns an `Err` — each
|
||||
/// handler checks for its own `None` case.
|
||||
fn dispatch(
|
||||
req: Request,
|
||||
store: &Todos,
|
||||
wake: &Notify,
|
||||
reminders: Option<&Reminders>,
|
||||
questions: Option<&Questions>,
|
||||
bus: &Bus,
|
||||
) -> Response {
|
||||
match req {
|
||||
|
|
@ -230,18 +214,6 @@ fn dispatch(
|
|||
Request::CancelReminder { id } => cancel_reminder(reminders, id),
|
||||
Request::CountPendingReminders => count_pending_reminders(reminders),
|
||||
Request::ReminderRollup { since_secs } => reminder_rollup(reminders, since_secs),
|
||||
Request::RecordAskedQuestion {
|
||||
id,
|
||||
target,
|
||||
question,
|
||||
} => record_asked_question(questions, id, &target, &question),
|
||||
Request::RecordAnsweringQuestion {
|
||||
id,
|
||||
asker,
|
||||
question,
|
||||
} => record_answering_question(questions, id, &asker, &question),
|
||||
Request::ClearQuestion { id } => clear_question(questions, id),
|
||||
Request::ListQuestions => list_questions(questions),
|
||||
Request::Compact { wake_prompt } => compact(bus, wake_prompt),
|
||||
}
|
||||
}
|
||||
|
|
@ -312,65 +284,6 @@ fn reminder_rollup(reminders: Option<&Reminders>, since_secs: u64) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
/// `RecordAskedQuestion` handler: mirror a question this agent asked.
|
||||
fn record_asked_question(
|
||||
questions: Option<&Questions>,
|
||||
id: i64,
|
||||
target: &str,
|
||||
question: &str,
|
||||
) -> Response {
|
||||
let Some(q) = questions else {
|
||||
return no_questions_store();
|
||||
};
|
||||
match q.record(id, Role::Asked, target, question) {
|
||||
Ok(()) => Response::Ok,
|
||||
Err(e) => err(&e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `RecordAnsweringQuestion` handler: mirror a question this agent owes a
|
||||
/// reply to.
|
||||
fn record_answering_question(
|
||||
questions: Option<&Questions>,
|
||||
id: i64,
|
||||
asker: &str,
|
||||
question: &str,
|
||||
) -> Response {
|
||||
let Some(q) = questions else {
|
||||
return no_questions_store();
|
||||
};
|
||||
match q.record(id, Role::Answering, asker, question) {
|
||||
Ok(()) => Response::Ok,
|
||||
Err(e) => err(&e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `ClearQuestion` handler: drop the mirror row for `id` (either role).
|
||||
fn clear_question(questions: Option<&Questions>, id: i64) -> Response {
|
||||
let Some(q) = questions else {
|
||||
return no_questions_store();
|
||||
};
|
||||
match q.clear(id) {
|
||||
Ok(count) => Response::Acked {
|
||||
count: u64::try_from(count).unwrap_or(0),
|
||||
},
|
||||
Err(e) => err(&e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `ListQuestions` handler: this agent's mirrored questions (both roles).
|
||||
fn list_questions(questions: Option<&Questions>) -> Response {
|
||||
let Some(q) = questions else {
|
||||
return no_questions_store();
|
||||
};
|
||||
match q.list() {
|
||||
Ok(rows) => Response::LooseEnds {
|
||||
loose_ends: rows.into_iter().map(question_to_loose_end).collect(),
|
||||
},
|
||||
Err(e) => err(&e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `UpsertTodo` handler: writes/refreshes a todo row, logs the outcome, and
|
||||
/// fires `wake` on a new-or-changed upsert so the serve loop runs a turn.
|
||||
fn upsert_todo(
|
||||
|
|
@ -511,34 +424,6 @@ fn no_reminders_store() -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
/// Shared "questions mirror unavailable" response for every question op
|
||||
/// when the store failed to open at boot (see `main.rs`'s best-effort open).
|
||||
fn no_questions_store() -> Response {
|
||||
Response::Err {
|
||||
message: "questions mirror unavailable on this agent".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a mirrored [`QuestionMirror`] to a [`LooseEnd::Question`]. `asker`/
|
||||
/// `target` are derived from `role` — this agent's own label fills whichever
|
||||
/// side `role` says is us, `peer` fills the other.
|
||||
fn question_to_loose_end(q: QuestionMirror) -> LooseEnd {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let age = u64::try_from(now.saturating_sub(q.asked_at.timestamp())).unwrap_or(0);
|
||||
let me = crate::identity::label();
|
||||
let (asker, target) = match q.role {
|
||||
Role::Asked => (me, Some(q.peer)),
|
||||
Role::Answering => (q.peer, Some(me)),
|
||||
};
|
||||
LooseEnd::Question {
|
||||
id: q.id,
|
||||
asker,
|
||||
target,
|
||||
question: q.question,
|
||||
age_seconds: age,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue