Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
5 changed files with 70 additions and 837 deletions

View file

@ -30,17 +30,10 @@ status(name)
interrupt(name, force?)
```
`start` and `continue` return once the turn is under way, not once it
finishes — a completion lands as a todo (`get_loose_ends`), same as any
other producer. `continue` takes the extra moment to confirm the resume
actually attached, so a `continue` naming a session that isn't there
fails the tool call outright instead of looking like it worked; the error
says which directory it searched, which is usually the fix (pass `dir`).
Use `status` for a
zero-cost "is it still going" check — for a running subagent it also
reports how long since that turn last produced output, so a few seconds
means it's working and an age climbing into the minutes means it's
wedged and worth an `interrupt`. Reach for `continue` only once you
`start` and `continue` return as soon as the process is confirmed
running, not once it finishes — a completion lands as a todo
(`get_loose_ends`), same as any other producer. Use `status` for a
zero-cost "is it still going" check; reach for `continue` only once you
actually have a new instruction for it, since that spends a turn.
`interrupt` genuinely stops a running turn (`force: true` for SIGKILL).

View file

@ -26,70 +26,12 @@ Served under the `subagent` MCP server (`mcp__subagent__<tool>`): `start`,
## State
In-memory only: what's running now, where each name's session lives, how
each name's last turn ended, and when each running turn last produced
output. All of it lives only as long as the daemon process does. A daemon
restart stops whatever was running rather than adopting it. The durable
record of a subagent's existence is claude's own on-disk session
(`hive_claude::SessionStore`), which `continue` reattaches to independent
of the daemon's own lifetime — a restart loses the _in-flight turn_, not
the subagent's history.
Because the remembered directory goes with the rest of it, a `continue`
after a restart has to re-supply `dir` when the session lives anywhere
other than the daemon's own working directory — and a restart is the
situation you reach for `continue` in most often.
## Is it working, or is it wedged?
`status` reporting **running** says a process is tracked, which a wedged
subagent satisfies as fully as a busy one. A running answer therefore
carries the age of that turn's last event too: seconds means it's working,
an age climbing into the minutes with no end-of-turn todo means it's
stuck. That one number replaces inferring the same thing from `ps` output
and CPU-time deltas.
Every line the subagent's `claude` process writes bumps the timestamp —
stream-json events, plain stdout chatter and stderr alike — and what the
subagent actually said is never read. The record says the child is alive,
not what it's doing. The clock starts at the spawn, so a subagent that wedged
before it ever emitted anything still reports a climbing age rather than
no age at all. It's dropped when the turn ends, since a finished turn has
no progress left to describe.
## A `continue` that finds no session
`continue` doesn't check for the session before spawning. claude's own
`--resume` is the authority, and it exits non-zero rather than quietly
starting a fresh session, so the check could only duplicate the lookup
the driver was about to do — while answering as though the session were
gone. The usual truth is that the session exists somewhere else.
`continue` waits for that answer instead. Where `start` returns the
instant the process exists — it creates its session, so the spawn
succeeding is the whole story — a resumed turn can fail a moment _after_
a pid exists, and a pid is no proof that turn began. `continue` holds the
tool call open until the turn is underway or the resume has come back
missed, and reports a miss as the call's own error, carrying claude's
message plus the location this daemon searched:
```
continue error: claude error: no session matched the requested id or title (searched /home/agent/.claude for cwd /home/agent/work; if it was started elsewhere, pass the `dir` it was started in)
```
The directory is the part claude's own message never names, and the part
that resolves the confusion: pass `dir` to point `continue` at the
directory the session was started in.
Nothing here is a fixed delay on the way to a successful turn. The wait
ends on whichever comes first — the turn's first stream event or its
early exit — and on this box both land inside a second, so a `continue`
that works answers about as fast as it did before. A five-second cap
bounds the one case neither covers: a child that neither speaks nor
exits, reported as started, with the end-of-turn todo left to say how it
goes. That todo still carries every failure that happens later in the
turn, exactly as before; the only one it no longer repeats is the miss
the caller has just been handed to its face.
In-memory only: a map of currently running processes, live only as long
as the daemon process is. A daemon restart stops whatever was running
rather than adopting it. The durable record of a subagent's existence is
claude's own on-disk session (`hive_claude::SessionStore`), which
`continue` reattaches to independent of the daemon's own lifetime — a
restart loses the _in-flight turn_, not the subagent's history.
## A killed turn

View file

@ -24,12 +24,6 @@ tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
# `test-util` for `#[tokio::test(start_paused = true)]`: the `continue`
# resume-grace tests assert what happens when the bound is actually reached,
# and paused time gets that answer without a five-second unit test.
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
# `hive-subagent-daemon` — long-running per-agent claude-subagent runner.
# Independent of `hive-bash-mcp` (own crate, own binary, own MCP server) —
# see lib.rs's module doc for why. Serves its MCP tools (`start`/

View file

@ -79,12 +79,8 @@ struct ContinueArgs {
#[serde(default)]
effort: Option<String>,
/// Omit to reuse whatever `dir` `start` (or a prior `continue`) used for
/// this name — the daemon remembers it until it restarts, and a restart
/// is exactly when you're most likely to be reaching for `continue`. So
/// pass it when pointing the session at a *different* directory than
/// last time, and pass it again after a restart if the session lives
/// anywhere other than the daemon's own working directory. A resume that
/// finds nothing says which directory it searched.
/// this name — the daemon remembers it. Only pass this to point the
/// session at a *different* directory than last time.
#[serde(default)]
dir: Option<String>,
}
@ -149,16 +145,12 @@ impl SubagentMcp {
description = "Give an existing named subagent session a new turn — whether that's \
because its previous turn finished and you have a follow-up instruction, or you're \
reattaching after this daemon restarted (the session itself survives independently \
of the daemon that spawned it). Returns once the turn is underway rather than the \
instant the process exists — a second or so, not the length of the turn — so a \
reply saying the turn started means it started; a name already running is refused. \
A name with no session to resume is not refused up front, because claude's own \
`--resume` decides that: a miss comes back as this call's own error, naming the \
directory that was searched, so check `dir` before concluding the session is gone. \
of the daemon that spawned it). Returns as soon as confirmed running, same as \
`start`. Refuses a name with no session on disk at all, or one already running. \
Resuming a session whose last turn was killed is allowed — the reply says so, since \
that turn's work stopped wherever it had got to."
)]
async fn r#continue(&self, Parameters(args): Parameters<ContinueArgs>) -> String {
fn r#continue(&self, Parameters(args): Parameters<ContinueArgs>) -> String {
match session::continue_(
&self.state,
&args.name,
@ -166,9 +158,7 @@ impl SubagentMcp {
args.model,
args.effort,
args.dir.as_deref(),
)
.await
{
) {
Ok(msg) => msg,
Err(e) => format!("continue error: {e:#}"),
}
@ -191,9 +181,7 @@ impl SubagentMcp {
#[tool(
description = "Report whether a subagent is currently running — a zero-cost check that \
never launches a process, unlike `continue`. The answer says what state it found \
and what to do about it. For a running one it also reports how long since that turn \
last produced any output, which is how you tell a subagent that's working from one \
that has wedged without resorting to `ps`."
and what to do about it."
)]
fn status(&self, Parameters(args): Parameters<StatusArgs>) -> String {
match session::status(&self.state, &args.name, args.dir.as_deref()) {

File diff suppressed because it is too large Load diff