claude's Bash tool starts every command in a new session, so a signal to claude's process group never reaches it. claude stops those commands itself when SIGINT makes it shut down. A SIGKILL leaves them running, re-parented to init. Per the ruling on #4999, every stop is SIGINT and nothing escalates. - Subagent `interrupt` always sends SIGINT. The `force` argument is gone from the MCP tool and from `session::interrupt`. A caller that still sends `force: true` gets SIGINT; the argument is ignored. - `interrupt` used to free the session name before it signalled, so a `continue` could start a second claude on the session while the first was still exiting. The `running` map now holds a three-state `Slot` (starting, running, stopping). An interrupted claude turn is `Stopping` until the turn task's own `finish_turn` clears it. `status` says "stopping", and `start`/`continue` on that name are refused. `interrupt` still returns at once. The ACP arm is unchanged: its name is freed when `session/cancel` is sent. - ACP `Connection` drop sends its process group SIGINT, not SIGKILL, and the spawn no longer sets tokio's `kill_on_drop`, which SIGKILLed the leader after the `Drop` body. The test fixture's background grandchild resets SIGINT to default, since `&` starts it ignored. Tests: a fake claude that records SIGINT, interrupted, fails with `cancel(true)` (the old `force: true` path). An interrupted fake claude that keeps running keeps its name claimed and `continue` is refused, which fails when the slot is freed before the signal. A fake ACP agent that records SIGINT, dropped, fails with SIGKILL in `Drop`, and also with SIGINT in `Drop` but `kill_on_drop(true)` left in place. Not changed: hive-claude's idle watchdog (still SIGKILL, separate hive-claude change), the units' `KillSignal`, `machinectl kill`, and the login helper.
74 lines
3.7 KiB
Markdown
74 lines
3.7 KiB
Markdown
# hive-runtime
|
|
|
|
The layer an agent's turns are driven through: one `Runtime` interface
|
|
(`run`, `compact`, `archive`, `canceller`, `choices`) with a backend per runtime.
|
|
|
|
- **claude** — `claude --print` through the `hive-claude` crate's
|
|
`InfiniteSession`. A pass-through: same spawn, same session handling, same
|
|
errors.
|
|
- **acp** — any [Agent Client Protocol](https://agentclientprotocol.com)
|
|
agent, spawned from a command, args and env handed to it (`RuntimeSpec`,
|
|
read from `HIVE_RUNTIME` / `HIVE_ACP_COMMAND` / `HIVE_ACP_ARGS` /
|
|
`HIVE_ACP_ENV`). It knows no agent by name; which agent runs, and how it is
|
|
configured, is decided in nix (`services.hyperhive.agent.runtime`,
|
|
`services.hyperhive.agent.acp.*`).
|
|
|
|
Both backends report a turn through `hive_claude::Sink` in claude's
|
|
`stream-json` shape. The ACP backend translates `session/update`
|
|
notifications into it (text and thought chunks as whole blocks, tool calls as
|
|
`tool_use` + `tool_result`, MCP tools named `mcp__<server>__<tool>`), so the
|
|
harness's stream consumers read either backend unchanged. Context usage comes
|
|
from ACP `usage_update`.
|
|
|
|
The crate depends on no hyperhive binary crate, so `hive-agent` and
|
|
`hive-subagent-mcp` can both drive turns through it.
|
|
|
|
## ACP backend: what it needs from the agent
|
|
|
|
- `mcpCapabilities.http` in its `initialize` response. The hyperhive tools
|
|
are only served over HTTP, so an agent without it is refused at startup.
|
|
- `loadSession`, to pick its session back up after a harness restart.
|
|
Without it every restart starts a new session.
|
|
|
|
## ACP backend: stopping a turn
|
|
|
|
A turn is stopped with `session/cancel`: by the `Canceller` handle, or by the
|
|
idle watchdog once no `session/update` has arrived for
|
|
`Config::idle_timeout`. An agent that has not answered the prompt 10s later
|
|
is sent SIGINT, and the next turn respawns it. A watchdog stop fails the turn
|
|
with `AcpError::IdleTimeout` (`IdleKilled` if the agent was sent SIGINT). This is
|
|
the only way a provider error the agent retries without reporting it, such as
|
|
an HTTP 429, ends the turn.
|
|
|
|
The agent leads its own process group. Stopping it, and dropping the runtime,
|
|
sends that group SIGINT, so the commands the agent started get it too.
|
|
Nothing escalates to SIGKILL: an agent that ignores SIGINT keeps running,
|
|
and the next turn starts a new one beside it.
|
|
|
|
## ACP backend: compaction
|
|
|
|
The same `CompactionPolicy` as the claude backend decides when: after a turn
|
|
past the watermark, or on `compact` (the operator's `/compact`, the agent's
|
|
`compact` tool).
|
|
|
|
- If the agent advertises a `compact` command (`available_commands_update`),
|
|
it is sent as the prompt `/compact` on the same session, as the ACP spec
|
|
runs any advertised command. A proactive compaction runs the policy's
|
|
checkpoint turn first, as on claude.
|
|
- Otherwise the policy's checkpoint turn runs, then the session is archived,
|
|
and the next turn starts a new one, carrying the system prompt again.
|
|
- A `compact` command that fails, or sends nothing for 180s (the turn's idle
|
|
window if that is shorter), is cancelled and handled as the case above; the
|
|
checkpoint turn is not run a second time. So a compaction always leaves a
|
|
smaller session behind, and a failed one is not retried on the next turn.
|
|
|
|
## ACP backend: model and effort
|
|
|
|
Before each prompt, `Config::model` and then `Config::effort` are set on the
|
|
session with `session/set_config_option`, as the value of its first `model`
|
|
and `thought_level` config option. A value the session does not offer, or
|
|
already holds, is not sent, so a claude alias such as `haiku` does nothing
|
|
on an ACP agent. Model goes first because the effort levels on offer depend
|
|
on it. The `Choices` handle (`Runtime::choices`) reads what the session
|
|
offers now, including after a `config_option_update`. It is empty until a
|
|
session is attached.
|