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

43
TODO.md
View file

@ -33,43 +33,12 @@ Pick anything from here when relevant. Cross-cutting design notes live in
## Manager → operator question channel
- **`mcp__hyperhive__ask_operator(question, options?)` tool** on the manager
MCP surface. The manager turn pauses; the question gets surfaced as a
prominent prompt on the dashboard (its own section, or interleaved with
the operator inbox); the operator's typed answer comes back as the tool
result. Modelled after Claude Code's `AskUserQuestion` tool.
Design open questions:
- **Storage.** New sqlite table `operator_questions(id, asker, question,
options_json, asked_at, answered_at, answer)` — or piggyback on the
existing message broker with a new envelope kind. Probably a new
table because the lifecycle (pending → answered) is different from
fire-and-forget messages.
- **Waiting semantics.** The MCP tool call needs to block until
answered. Two options:
1. Long-poll from inside the tool handler (broker-style — broadcast
on insert, await via `tokio::sync::broadcast`). Simple but the
claude turn stays alive for the whole wait, eating context-window
budget.
2. Tool returns a `question_id` immediately; manager re-enters its
inbox loop and a `HelperEvent::OperatorAnswered { id, answer }`
wakes it. Cheaper context-wise but two-step.
- **Dashboard UX.** New "◆ M1ND H4S QU3STI0NS ◆" section at the top
when any question is pending. Inline `<form>` with a textarea (or
select if `options` were provided), POST `/api/answer-question`.
State refresh + the live SSE stream notify the manager harness.
- **Sub-agent path.** Sub-agents don't get the tool — they message the
manager and the manager decides whether to relay the question to the
operator. The manager's system prompt already covers this.
- **Timeout / cancel.** Questions that sit pending too long: do they
expire? Manager probably wants to know if the operator hasn't
answered after some interval so it can fall back. Maybe a per-
question `ttl_seconds`.
- **TTL / cancel on `ask_operator`.** Questions today block forever; the
manager turn stays alive until the operator answers. Add a per-question
`ttl_seconds` (or a dashboard "cancel" button that resolves the question
with a sentinel answer) so a long-idle question can time out and let the
manager fall back. Wire the timeout into `OperatorQuestions::wait_answered`
and surface remaining-time on the dashboard.
## Loop substance