When an ACP agent answered session/set_config_option with an error,
choose() only logged it, so the session's current value never moved and
offered_pickers() kept treating the refused model as pending: the picker
showed the refused model as current and hid the effort picker.
hive-runtime now keeps the refused value per category on Choices, exposed
as Choice::refused, set on the RPC error and cleared when the agent next
accepts a value for that category. offered_pickers() treats a refused
value as not settable, so the picker shows the session's actual model and
its effort levels.
The fake ACP agent gains REFUSE, which errors the first
session/set_config_option to that value.
Refs #4832 (ACP half only)
An ACP agent's model and effort pickers now list what its session offers
(its `model` and `thought_level` config options) instead of the claude
model list and EFFORT_LEVELS. A pick goes through the same Bus::set_model /
Bus::set_effort -> Config.model / Config.effort path as on claude; before
each prompt the ACP runtime sets it with `session/set_config_option`, model
first, and only when the session offers that value. Options are re-read
from the set response and from `config_option_update`, so the effort picker
disappears when the chosen model offers no effort levels, and is hidden
while a newly picked model waits for the next turn.
The session's options reach the web UI through a `Choices` handle from a
new `Runtime::choices`, registered on the bus the way `canceller` is.
/api/model and /api/effort accept only offered values on ACP. The claude
path is unchanged.
Refs #4391
A checkpoint or `compact` command turn can end with a bare `end_turn`
as a normal answer, since the agent does the work on its side. Treating
it as `EmptyEndTurn` made `compact_session` count every such compaction
as failed and archive the session. `prompt()` and `turn()` now take a
`TurnKind`; only `run()`'s ordinary turns get the check.
The test agent's `BLANK` env answers one prompt text with a bare
`end_turn`, for the compact and checkpoint tests.
opencode answers `end_turn` even when its provider rejected the request
with a non-retryable error (401, 400), and forwards nothing over ACP, so
the turn looked like an empty success. A turn that ends with `end_turn`,
no event, no `usage_update` and no `usage` in the prompt response now
fails with `AcpError::EmptyEndTurn`.
A turn that really produced nothing and reported no usage is reported
the same way; that false positive is accepted.
The test agent's plain `end_turn` replies now carry a response `usage`,
so its ordinary turns stay successes; a response `usage` feeds only cost
telemetry, not the compaction watermark. A new `blank` mode keeps the
empty reply for the error case.
A `compact` command that errors, or sends nothing, used to leave the
session as full as before: `compacted` stayed false, so every following
turn ran the checkpoint and another `/compact` again, each one waiting
out the 600s turn idle window when the command was silent.
- The `/compact` turn gets its own idle bound, COMPACT_IDLE (3 min, or
the turn's idle window if shorter), through the turn's existing
watchdog, so a silent command is cancelled (or killed) like any
stalled turn.
- When the command fails or hits that bound, compaction falls back to
the no-command path: the session is archived and the next turn starts
a new one. The checkpoint turn runs there only if it has not already
run in this compaction.
- Compaction returns early when attaching produced a new session (a
failed `session/load` or a never-answered first prompt): there is
nothing in it to compact.
Refs #4391
An ACP agent's session is now compacted like a claude one: proactively
once a turn crosses the percent-of-window watermark, and on the
operator's /compact or the agent's compact tool. Before, the ACP
backend's compact returned Unsupported and no watermark applied to it.
- AcpRuntime takes the same CompactionPolicy as ClaudeRuntime;
make_session builds one PercentPolicy (with CHECKPOINT_PROMPT) and hands
it to whichever backend runs.
- The runtime keeps the commands each session advertises in
available_commands_update. If `compact` is among them, compaction sends
the prompt `/compact` on the same session, which is how the ACP spec
runs an advertised command. A proactive compaction runs the checkpoint
turn first, as InfiniteSession does.
- With no compact command, the checkpoint turn runs, the session is
archived, and the next turn starts a new one with the system prompt.
- Error::Unsupported had no producer left, so it and drive_turn's
"/compact skipped" arm are gone.
Refs #4391
`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 new session is only recorded once its first prompt is answered, so a
failed first prompt made the next turn run `session/new` again and left the
first session behind in the agent: in the same process, and after a restart.
The new session's id is now kept in `<session file>.pending` until that
prompt is answered. The next turn attaches it (in-process, or through
`session/load` after a restart) and still prepends the system prompt, since
the session has not had an answered prompt yet. Recording the session
removes the pending file, and `archive` drops it.
Raised by argus in the #4812 review.
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