feat(#2635): wire harness-local questions mirror (inc2 pt2)

This commit is contained in:
damocles 2026-07-23 21:10:51 +02:00 committed by mara
commit e5ef5a72be
8 changed files with 465 additions and 191 deletions

View file

@ -35,9 +35,9 @@ pub use args::{
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
use render::{
dial_agent_socket, format_matrix_summary, local_reminders, local_todos, loose_end_kind_label,
mark_local_todo_done, matrix_unread_summary, parse_loose_end_kind, render_loose_ends,
reply_err,
dial_agent_socket, format_matrix_summary, local_questions, local_reminders, local_todos,
loose_end_kind_label, mark_local_todo_done, matrix_unread_summary, parse_loose_end_kind,
render_loose_ends, reply_err,
};
/// Write (or remove) the status file in the agent's own `state/` directory.
@ -186,9 +186,13 @@ impl AgentServer {
Ok(to) => to,
Err(reason) => return format!("invalid `to` agent name: {reason}"),
};
let question = args.question;
let target = to
.as_ref()
.map_or_else(|| "operator".to_owned(), std::string::ToString::to_string);
let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::Ask {
question: args.question,
question: question.clone(),
options: args.options,
multi: args.multi,
ttl_seconds: args.ttl_seconds,
@ -196,10 +200,22 @@ impl AgentServer {
})
.await;
let s = match resp {
Ok(hive_core_agent_sock::Response::QuestionQueued { id }) => format!(
"question queued (id={id}); answer will arrive as a system \
`question_answered` event in your inbox"
),
Ok(hive_core_agent_sock::Response::QuestionQueued { id }) => {
// Best-effort local questions-mirror record — a dial
// failure just means `get_loose_ends` won't show this
// row locally; the actual question is already queued
// in c0re regardless.
let _ = dial_agent_socket(&hive_agent_sock::Request::RecordAskedQuestion {
id,
target,
question,
})
.await;
format!(
"question queued (id={id}); answer will arrive as a system \
`question_answered` event in your inbox"
)
}
other => reply_err(other, "ask"),
};
annotate_retries(s, retries)
@ -226,6 +242,11 @@ impl AgentServer {
answer: args.answer,
})
.await;
if resp.is_ok() {
// Best-effort — this agent is done owing a reply for `id`;
// drop its `answering` mirror row.
let _ = dial_agent_socket(&hive_agent_sock::Request::ClearQuestion { id }).await;
}
annotate_retries(
format_ack(resp, "answer", format!("answered question {id}")),
retries,
@ -350,6 +371,16 @@ impl AgentServer {
if is_self_query && let Some(reminders) = local_reminders().await {
loose_ends.extend(reminders);
}
// Merge local mirrored questions — same self-query-only
// restriction as todos/reminders above. c0re no longer sources
// `Question` rows for `for_agent`/`hive_wide` (see
// `hive-c0re::loose_ends` doc), so this is the only place a
// self-query sees its own questions now; a manager query for a
// child still sees that child's approvals (unaffected) but no
// longer its questions, matching the reminders precedent.
if is_self_query && let Some(questions) = local_questions().await {
loose_ends.extend(questions);
}
annotate_retries(render_loose_ends(&loose_ends), retries)
})
.await
@ -476,6 +507,15 @@ impl AgentServer {
let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::CancelLooseEnd { kind, id })
.await;
if resp.is_ok() && kind == hive_sh4re::CancelLooseEndKind::Question {
// Best-effort — cancel is ownership-gated to the asker on
// the c0re side, so a successful cancel here always means
// *this* agent's own `asked` mirror row for `id`. Known gap
// (documented on `Questions`): the target isn't notified,
// so their `answering` row lingers until they call
// `answer()` or it goes stale.
let _ = dial_agent_socket(&hive_agent_sock::Request::ClearQuestion { id }).await;
}
annotate_retries(
format_ack(
resp,

View file

@ -351,6 +351,17 @@ pub(super) async fn local_reminders() -> Option<Vec<hive_sh4re::LooseEnd>> {
}
}
/// Query the harness's in-agent socket for this agent's mirrored questions
/// (both roles — asked and answering). Same best-effort
/// contract as [`local_reminders`]; c0re stays the actual `Ask`/`Answer`
/// routing, this only mirrors the durable "still owed a reply" view.
pub(super) async fn local_questions() -> Option<Vec<hive_sh4re::LooseEnd>> {
match dial_agent_socket(&hive_agent_sock::Request::ListQuestions).await? {
hive_agent_sock::Response::LooseEnds { loose_ends } => Some(loose_ends),
_ => None,
}
}
/// Mark one of this agent's local todos (loose-ends v2) done by id, via
/// the harness's in-agent socket — reachable through `cancel_loose_end`
/// kind `"todo"` so clearing a todo never has to shell out through a