The reader loops used while-let Ok(Some(line)), so a read error ended them
exactly like EOF with nothing logged. next_line now warns on Err, naming
the stream, and returns None; the close path is unchanged.
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.
The ACP agent was spawned with only `kill_on_drop`, which SIGKILLs the
agent's own pid. Commands the agent had started (a `nix build`, a test
run) were reparented and kept running after the agent was killed: when a
subagent run ends after `interrupt`, when an agent ignores
`session/cancel` past the grace, and when the harness drops the runtime.
The agent now leads its own process group (`process_group(0)`), and
dropping its `Connection` SIGKILLs that group. The soft path is unchanged:
`interrupt` / `Canceller::cancel` still sends `session/cancel`.
Refs #4999. The claude half needs hive-claude 0.1.3, which is not on
crates.io yet.
An opencode ACP agent got its provider API key only from the hand-placed
backendEnvironmentFile. It now also reads it from the swarm secret store
at swarm/agents/<agent>/acp-provider, field api_key, under its own
certificate, and sets it in the spawned ACP agent's environment only.
Nothing is written to disk.
Precedence: a value already in the process environment (the env file)
wins and the store is not asked. Otherwise the stored key is used when
present. With no store, nothing stored, or a failed read, the agent is
spawned without the key as before, and one line is logged without the
value.
The variable name comes from the existing per-agent option
acp.opencode.provider.apiKeyEnv, exported as HIVE_ACP_API_KEY_ENV on the
harness only for the opencode preset. Other ACP commands are unchanged.
The read lives in hive-runtime, where the ACP child is spawned, so both
hive-agent and hive-subagent-daemon use it. The subagent daemon unit
gets the key name and, when the agent has a store, the agent's store
identity (the same credentials queue-identity.nix gives the harness).
No new option or setting. Closes#4841.
`Runtime` gets a fourth operation, `canceller()`: a handle that stops the
turn in flight from outside `run`. The ACP backend returns one; claude
returns `None`, because the harness stops a claude turn by signalling the
`claude` process, and that path is unchanged.
Both stops send the agent `session/cancel`:
- `Canceller::cancel()`, when asked from outside. The turn then ends
normally, reported with stop reason `cancelled` whatever reason the agent
gives. opencode 1.15.10, for one, answers a cancelled prompt with
`end_turn` (`acp/agent.ts` `prompt()` always returns `end_turn`).
- The idle watchdog, once no `session/update` has arrived for
`Config::idle_timeout`, the same field claude's watchdog reads. The turn
fails with `AcpError::IdleTimeout`.
An agent that has not answered the prompt 10s after `session/cancel` is
killed (`IdleKilled` / `CancelIgnored`), and the next turn respawns it.
The watchdog is also what ends a turn stuck on a provider HTTP 429.
opencode 1.15.10 retries a retryable provider error with no attempt limit
(`session/retry.ts` `policy`, `session/processor.ts` `Effect.retry`) and
forwards neither `session.status` nor `session.error` over ACP (its
`handleEvent` only handles `permission.asked` and `message.part.*`). So the
ACP client sees nothing at all until the provider recovers. The
`IdleTimeout` message says a silently retried provider error looks like
this.
Refs #4391
A permission request counted as an MCP tool call whenever its title
looked like `<server>_<tool>`, whatever its kind, and acp_permits
allowed MCP calls before looking at the kind. So an `execute` request
titled e.g. `hyperhive_x`, or a `fetch` without web_tools, was allowed.
MCP tool calls come with kind `other` (opencode's toToolKind maps every
tool it doesn't name, MCP tools included, to "other"). The runtime now
sets PermissionAsk::mcp_server only for kind `other`, and acp_permits
allows an MCP server's tool only under the `other` arm.
Refs #4391
The session id was written to the session file right after session/new,
but the system prompt rides on the first prompt only. If that prompt
failed, the next turn (or the next harness start) resumed the recorded
session as not-new and the system prompt never reached it.
The id is now written after the first session/prompt gets its reply.
A failed first prompt leaves nothing recorded, so the next turn starts
a new session and sends the system prompt again. Chosen over a separate
"system prompt delivered" marker: one file, and "recorded" already means
"usable".
Tests drive AcpRuntime against a scripted sh agent that fails the first
prompt: in-process and across a restart, the retry is a new session
carrying the system prompt.
Also: PermissionPolicy now sees a PermissionAsk (kind plus the MCP
server the tool belongs to, matched by name against the servers handed
to the session), so a caller can tell MCP tool calls from other `other`
requests.
Refs #4391
A `Runtime` trait (run / compact / archive) with two backends:
- claude: a pass-through to hive_claude's InfiniteSession and
SessionStore, so a claude turn is the same spawn, session handling
and errors as before.
- acp: a generic Agent Client Protocol client. It spawns the command,
args and env from RuntimeSpec (HIVE_RUNTIME / HIVE_ACP_COMMAND /
HIVE_ACP_ARGS / HIVE_ACP_ENV), refuses an agent whose
mcpCapabilities.http is not true, passes the claude --mcp-config
servers as ACP mcpServers, keeps one session id in a file
(session/load after a restart, session/new otherwise), and maps
session/update into claude stream-json events plus usage_update into
Telemetry. Permission requests are answered by a caller-supplied
policy on the ACP tool kind. compact returns Unsupported for now.
The crate depends on no hyperhive binary crate, so the subagent
daemon can move onto it without pulling in hive-agent.
Refs #4391