Watch
0
0
Fork
You've already forked hyperhive
0

hive-runtime: cancel and idle watchdog for ACP turns

`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
This commit is contained in:
atlas 2026-09-29 23:22:50 +02:00
commit bcbeb8ac9c
6 changed files with 333 additions and 28 deletions

View file

@ -4,7 +4,7 @@ use std::path::PathBuf;
use hive_claude::{CompactionPolicy, Config, InfiniteSession, Progress, SessionStore, Sink};
use crate::{Result, Runtime};
use crate::{Canceller, Result, Runtime};
/// `claude --print` turns on a titled [`InfiniteSession`], which owns
/// resume-or-create and compaction.
@ -39,4 +39,8 @@ impl<P: CompactionPolicy> Runtime for ClaudeRuntime<P> {
fn archive(&self) -> Result<Option<PathBuf>> {
Ok(self.store.archive_by_title(&self.title)?)
}
fn canceller(&self) -> Option<Canceller> {
None
}
}