subagent: make a missed continue the tool call's own error
`continue` returned "started" the instant `Claude::spawn` handed back a pid, and a resume that matched nothing only surfaced later, as an end-of-turn todo. By then the caller had moved on believing it had a running subagent. A pid is proof enough for `start`, which creates its session: the spawn succeeding is the whole story. It is not proof for a resume — claude exits non-zero a fraction of a second *after* the process exists. So `continue` now waits for the first real answer and reports a miss as its own `Err`, carrying claude's message and the directory searched. The wait ends on whichever comes first, so a successful `continue` pays no fixed delay: the turn's first non-terminal stream event settles it at about the same moment a miss's exit would have. Measured on this box: 14 runs of the driver's own invocation against a missing session took 550-1087 ms spawn to exit, and a healthy turn's first event lands at roughly 500 ms. The five-second cap is ~4.6x the slowest miss and is only ever reached by a child that neither speaks nor exits. The underway signal reads the event's kind, not its content: a missed resume is not silent — it emits a terminal `result` event and stderr before exiting — so "any sink callback" would have reported every miss as a successful start. Liveness still counts all three callbacks. The end-of-turn todo is unchanged for every failure later in the turn; the only one it no longer repeats is the miss the caller was just handed. Refs #4405
This commit is contained in:
parent
31c76ddf32
commit
6e2de33f26
5 changed files with 385 additions and 52 deletions
|
|
@ -149,14 +149,16 @@ 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 as soon as confirmed running, same as \
|
||||
`start`; a name already running is refused. A name with no session to resume is not \
|
||||
refused up front — claude's own `--resume` decides that, and a miss arrives as a \
|
||||
failed turn naming the directory that was searched, so check `dir` before concluding \
|
||||
the session is gone. 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."
|
||||
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. \
|
||||
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."
|
||||
)]
|
||||
fn r#continue(&self, Parameters(args): Parameters<ContinueArgs>) -> String {
|
||||
async fn r#continue(&self, Parameters(args): Parameters<ContinueArgs>) -> String {
|
||||
match session::continue_(
|
||||
&self.state,
|
||||
&args.name,
|
||||
|
|
@ -164,7 +166,9 @@ impl SubagentMcp {
|
|||
args.model,
|
||||
args.effort,
|
||||
args.dir.as_deref(),
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(e) => format!("continue error: {e:#}"),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue