ask_operator tool: non-blocking; operator answer arrives as helper event

new mcp tool on the manager surface that queues a question on the
dashboard and returns the question id immediately. operator submits an
answer via /answer-question/<id>; the dashboard fires
HelperEvent::OperatorAnswered { id, question, answer } into the manager
inbox so the next turn picks it up.

also: fix async-form button stuck on spinner after successful submit
(refreshState skipped re-rendering, so the button was never re-enabled).
This commit is contained in:
müde 2026-05-15 18:44:42 +02:00
parent abfd2cce4b
commit 2770630f33
17 changed files with 426 additions and 79 deletions

View file

@ -170,7 +170,11 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
turn::emit_turn_end(&bus, &outcome);
}
Ok(ManagerResponse::Empty) => {}
Ok(ManagerResponse::Ok | ManagerResponse::Status { .. }) => {
Ok(
ManagerResponse::Ok
| ManagerResponse::Status { .. }
| ManagerResponse::QuestionQueued { .. },
) => {
tracing::warn!("recv produced unexpected response kind");
}
Ok(ManagerResponse::Err { message }) => {
@ -184,7 +188,6 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
}
}
/// Per-turn user prompt. The role/tools/etc. is in the system prompt
/// (`prompts/manager.md` → `claude --system-prompt-file`); this is just
/// the wake signal. `unread` is the inbox depth after this message was
@ -193,7 +196,9 @@ fn format_wake_prompt(from: &str, body: &str, unread: u64) -> String {
let pending = if unread == 0 {
String::new()
} else {
format!("\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)")
format!(
"\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)"
)
};
format!("Incoming message from `{from}`:\n---\n{body}\n---{pending}")
}