subagent: give each run its own signal URL, and drop the name argument

`goal_reached`/`need_help` took the session name as a tool argument, so
identity was an assertion by the caller and the only guard on it was
`occupancy()` — "does that name have a turn in flight", which two
concurrently running siblings both satisfy for each other. A subagent
could stop its sibling's run by naming it.

Identity moves into the URL. Each spawned run is minted an unguessable
token (`Uuid::new_v4`, the OS CSPRNG), the URL carrying it goes into that
one subagent's own `--mcp-config`, and the route resolves it back to a
session before dispatching to a handler bound to that session. Neither
tool takes a `name` any more: a subagent has no field in which to name a
sibling, and a sibling's name — which a brief may well mention — is not a
token.

One route with a path parameter, not a route per session: the `Router` is
built once at startup and subagents come and go for the daemon's whole
life. An unminted or revoked token gets a bare 404, the same answer either
way, so nothing enumerates. A run's token is revoked when the run ends
(`finish_turn`) or when a call never reached a spawn.

Two things fall out of that:

- the config file becomes one per session. A single shared path was
  already a race between two `start`s; with a per-session URL in it, the
  loser would read the winner's identity.
- `occupancy()` stops being the identity guard and is gone from the signal
  path entirely rather than kept "just in case" — a revoked token can't
  reach it, and it never answered the question it was standing in for.
  It still backs `status`, which is what it was always actually for.

Refs #4403
Refs #4413
This commit is contained in:
atlas 2026-09-14 22:24:51 +02:00
commit 34129d776c
11 changed files with 575 additions and 180 deletions

View file

@ -32,13 +32,34 @@ routes rather than six tools on one, so that being able to report on a run
never carries the ability to start one: there's no route a subagent holds
that `start` is reachable from.
## A subagent can't name a session, not even its own
Neither signal tool takes a session name. **The URL is the identity.** At
each spawn the daemon mints that run an unguessable token, serves it at
`/signal/mcp/<token>`, and writes that one URL into that one subagent's own
`--mcp-config` — a file per session, not a shared one. A request is
resolved to a session before it's dispatched, and the tools read the
session off the resolution.
A subagent therefore has no field in which to name a sibling, and knowing
a sibling's name buys nothing: a name isn't a token. Two subagents running
concurrently can't signal each other — which the earlier shape, a shared
route plus a `name` argument guarded by a liveness check, allowed.
A token that resolves to nothing — never minted, or revoked when its run
ended — gets a bare **404**, the same answer for every token, so nothing
about the refusal says whether some other session exists.
## State
In-memory only: what's running now, where each name's session lives, how
each name's last turn ended, when each running turn last produced output,
what each session is working toward, how far through its turn budget it
is, why its run stopped, and where it writes its report. All of it lives
only as long as the daemon process does. A daemon
is, why its run stopped, where it writes its report, and which signal
token belongs to it. All of it lives only as long as the daemon process
does — so a daemon restart invalidates every signal URL it had issued,
which is the same thing as it having stopped the runs those URLs belonged
to. 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
@ -213,5 +234,8 @@ Set `hyperhive.extraMcpServers.<name>.availableToSubagents = true` on a
specific entry to hand that one server to subagents as well — useful for,
say, a read-only lookup or scraper MCP a subagent's bounded, single-batch
task might need. `hive-subagent-mcp`'s `mcp_config` module renders the
opted-in subset into its own `--mcp-config` file per turn; an entry left
at the default `false` never appears there.
opted-in subset into a `--mcp-config` file per session, rewritten each
turn; an entry left at the default `false` never appears there. Per
session rather than one shared file, because the `subagent_control` entry
in it carries that session's own signal URL — one file for everyone would
be a race over whose identity each subagent reads at startup.