hive-agent: remove the in-container questions mirror

This commit is contained in:
damocles 2026-08-29 21:28:37 +02:00
commit c865114743
7 changed files with 59 additions and 528 deletions

View file

@ -22,7 +22,6 @@ mod otel_turn_metrics;
mod paths;
mod plugins;
mod prompt;
mod questions;
mod reminder_timer;
mod reminders;
mod serve_common;
@ -109,17 +108,7 @@ async fn main() -> Result<()> {
/// `ContainerCrash`, reparent notifications, and friends; the parse
/// and log path is identical. Quiet no-op when `from` isn't
/// `SYSTEM_SENDER`.
///
/// Also keeps the harness-local questions mirror in sync on the two
/// question-shaped events: `QuestionAsked` (a peer/manager asked *this*
/// agent — mirror an `Answering` row) and
/// `QuestionAnswered` (a question *this* agent asked got a reply —
/// clear the mirrored `Asked` row). Best-effort loopback dial of the
/// in-agent socket (see `todo_server::dial`'s docs on why a dial beats
/// threading an `Arc<Questions>` through this whole call chain) — a
/// dial failure is a missed mirror update, not a turn failure, so it's
/// logged and swallowed rather than propagated.
async fn log_system_event(bus: &Bus, from: &str, body: &str) {
fn log_system_event(bus: &Bus, from: &str, body: &str) {
if from != SYSTEM_SENDER {
return;
}
@ -129,62 +118,11 @@ async fn log_system_event(bus: &Bus, from: &str, body: &str) {
} else {
tracing::info!(%from, %body, "system message");
}
match parsed {
Some(HelperEvent::QuestionAsked {
id,
asker,
question,
..
}) => {
mirror_question(hive_agent_sock::Request::RecordAnsweringQuestion {
id,
asker,
question,
})
.await;
}
Some(HelperEvent::QuestionAnswered { id, .. }) => {
mirror_question(hive_agent_sock::Request::ClearQuestion { id }).await;
}
_ => {}
}
bus.emit(LiveEvent::Note {
text: format!("[system] {body}"),
});
}
/// Open the harness-local questions mirror against the consolidated state
/// db — same open-alongside shape as the reminders store
/// (`reminders::Reminders::open`) opened just above this call site in
/// `serve_main`, sharing the same file (distinct table, see
/// `questions::SCHEMA`). `None` on open failure disables question ops the
/// same way a failed reminders open disables reminder ops
/// (`no_questions_store` in `todo_server`). Pulled out to its own fn to
/// keep `serve_main` under the pedantic line-count lint.
fn open_question_store() -> Option<Arc<questions::Questions>> {
match questions::Questions::open(&paths::state_db()) {
Ok(store) => Some(Arc::new(store)),
Err(e) => {
tracing::error!(error = ?e, "open questions db failed — question mirror disabled");
None
}
}
}
/// Fire-and-forget helper for `log_system_event`'s questions-mirror dial:
/// logs a warning on dial failure / an `Err` response, otherwise silent.
async fn mirror_question(req: hive_agent_sock::Request) {
match todo_server::dial(&req).await {
Some(hive_agent_sock::Response::Err { message }) => {
tracing::warn!(%message, ?req, "questions mirror dial returned an error");
}
None => {
tracing::warn!(?req, "questions mirror dial failed (socket unavailable?)");
}
Some(_) => {}
}
}
/// Body string for the turn-failure notification we route to
/// `<parent>` on `TurnError::Failed`. Reads the hive-qualified
/// identity so the receiver sees `agent@hive` rather than relying on
@ -482,23 +420,22 @@ impl Surface for AgentSurface {
// ---------- generic turn loop ----------
/// Opens the todos store and spawns the in-agent todo socket (loose-ends
/// v2 + harness-local reminders + questions): the harness owns the todo +
/// reminder + question stores locally and serves the in-container
/// producers on `HIVE_AGENT_SOCKET`. A new/changed todo upsert fires the
/// returned `Notify` so the serve loop drives a turn directly — no broker
/// v2 + harness-local reminders): the harness owns the todo + reminder
/// stores locally and serves the in-container producers on
/// `HIVE_AGENT_SOCKET`. A new/changed todo upsert fires the returned
/// `Notify` so the serve loop drives a turn directly — no broker
/// round-trip, no marker files. Best-effort: if the todos store can't
/// open, the whole socket isn't served (reminder + question ops ride
/// along on the same listener, so they're gated on the same store —
/// acceptable since a from-scratch harness boot either has a writable
/// harness dir or doesn't). Split out of `serve_main` to keep it under
/// clippy's `too_many_lines` limit; kept alongside the returned `Notify`
/// so the serve loop's `LocalTodo` arm can gate a wake on `has_any()`
/// before spawning a turn — see its doc comment (the phantom-todo-wake
/// issue: a burst of same-turn upserts can arm a second `Notify` permit
/// that outlives the turn that already drained its payload).
/// open, the whole socket isn't served (reminder ops ride along on the
/// same listener, so they're gated on the same store — acceptable since a
/// from-scratch harness boot either has a writable harness dir or
/// doesn't). Split out of `serve_main` to keep it under clippy's
/// `too_many_lines` limit; kept alongside the returned `Notify` so the
/// serve loop's `LocalTodo` arm can gate a wake on `has_any()` before
/// spawning a turn — see its doc comment (the phantom-todo-wake issue: a
/// burst of same-turn upserts can arm a second `Notify` permit that
/// outlives the turn that already drained its payload).
fn spawn_todo_socket(
reminder_store: Option<Arc<reminders::Reminders>>,
question_store: Option<Arc<questions::Questions>>,
bus: &Bus,
) -> (Arc<tokio::sync::Notify>, Option<Arc<todos::Todos>>) {
let todo_wake = Arc::new(tokio::sync::Notify::new());
@ -509,14 +446,8 @@ fn spawn_todo_socket(
let bus_for_socket = bus.clone();
let store_for_socket = store.clone();
tokio::spawn(async move {
if let Err(e) = todo_server::run(
store_for_socket,
wake,
reminder_store,
question_store,
bus_for_socket,
)
.await
if let Err(e) =
todo_server::run(store_for_socket, wake, reminder_store, bus_for_socket).await
{
tracing::error!(error = %e, "in-agent todo socket exited with error");
}
@ -646,9 +577,7 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
}
};
tokio::spawn(reminder_timer::run(reminder_store.clone(), reminder_tx));
let question_store = open_question_store();
let (todo_wake, todos_store) =
spawn_todo_socket(reminder_store.clone(), question_store.clone(), &bus);
let (todo_wake, todos_store) = spawn_todo_socket(reminder_store.clone(), &bus);
if matches!(initial, LoginState::NeedsLogin) {
login::wait_for_login(
&claude_dir,
@ -973,7 +902,7 @@ async fn handle_turn<S: Surface>(
let body = first.body;
let redelivered = first.redelivered;
let msg_id = first.id;
log_system_event(bus, &from, &body).await;
log_system_event(bus, &from, &body);
tracing::info!(%from, %body, %redelivered, "inbox");
let unread = S::inbox_unread(socket).await;
bus.emit(LiveEvent::TurnStart {