feat(#2635): wire harness-local questions mirror (inc2 pt2)
This commit is contained in:
parent
9471201698
commit
e5ef5a72be
8 changed files with 465 additions and 191 deletions
|
|
@ -84,21 +84,82 @@ 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`.
|
||||
fn log_system_event(bus: &Bus, from: &str, body: &str) {
|
||||
///
|
||||
/// 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) {
|
||||
if from != SYSTEM_SENDER {
|
||||
return;
|
||||
}
|
||||
let parsed = serde_json::from_str::<HelperEvent>(body).ok();
|
||||
if let Some(event) = parsed {
|
||||
if let Some(event) = &parsed {
|
||||
tracing::info!(?event, "helper event");
|
||||
} 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
|
||||
|
|
@ -393,6 +454,56 @@ 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
|
||||
/// 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).
|
||||
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());
|
||||
let todos_store: Option<Arc<todos::Todos>> = match todos::Todos::open(&paths::state_db()) {
|
||||
Ok(store) => {
|
||||
let store = Arc::new(store);
|
||||
let wake = todo_wake.clone();
|
||||
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
|
||||
{
|
||||
tracing::error!(error = %e, "in-agent todo socket exited with error");
|
||||
}
|
||||
});
|
||||
Some(store)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "open todos db failed — in-agent todo socket disabled");
|
||||
None
|
||||
}
|
||||
};
|
||||
(todo_wake, todos_store)
|
||||
}
|
||||
|
||||
/// Boot — wires up the web UI, login state, stats, plugins, forge
|
||||
/// notifier, and either drops into `serve_loop` directly (`Online`) or
|
||||
/// parks on the login flow first (`NeedsLogin`). See
|
||||
|
|
@ -491,42 +602,9 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
}
|
||||
};
|
||||
tokio::spawn(reminder_timer::run(reminder_store.clone(), reminder_tx));
|
||||
// In-agent todo socket (loose-ends 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 `todo_wake` 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 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).
|
||||
let todo_wake = Arc::new(tokio::sync::Notify::new());
|
||||
// Kept alongside `todo_wake` 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).
|
||||
let todos_store: Option<Arc<todos::Todos>> = match todos::Todos::open(&paths::state_db()) {
|
||||
Ok(store) => {
|
||||
let store = Arc::new(store);
|
||||
let wake = todo_wake.clone();
|
||||
let reminders = reminder_store.clone();
|
||||
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, reminders, bus_for_socket).await
|
||||
{
|
||||
tracing::error!(error = %e, "in-agent todo socket exited with error");
|
||||
}
|
||||
});
|
||||
Some(store)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = ?e, "open todos db failed — in-agent todo socket disabled");
|
||||
None
|
||||
}
|
||||
};
|
||||
let question_store = open_question_store();
|
||||
let (todo_wake, todos_store) =
|
||||
spawn_todo_socket(reminder_store.clone(), question_store.clone(), &bus);
|
||||
if matches!(initial, LoginState::NeedsLogin) {
|
||||
login::wait_for_login(&claude_dir, login_state.clone(), &bus, poll_ms).await;
|
||||
} else {
|
||||
|
|
@ -690,7 +768,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);
|
||||
log_system_event(bus, &from, &body).await;
|
||||
tracing::info!(%from, %body, %redelivered, "inbox");
|
||||
let unread = S::inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
|
|
|
|||
Loading…
Reference in a new issue